Skip to content

Commit 68632aa

Browse files
committed
Unexpected Namespacing in rels File
Fix PHPOffice#3720. Third-party product created a spreadsheet which PhpSpreadsheet could not read because of unexpected namespacing in workbook.xml.rels. The file which demonstrated the problem was attached to PHPOffice#3423, however I do not believe it was related to the original problem. Nevertheless, the original issue specifically called out Protection, so I put some Protection tests in the validation test for the fix. In doing so, I found that Style/Protection is particularly confusing. Its properties will often have the value `inherit`, which isn't all that helpful; and, even when the `locked` value is `protected`, the cell won't actually be locked unless the sheet is protected as well. The `hidden` property is even more obscure - it applies only to formulas, and refers to hiding the property on the formula bar, not in the cell. I have added methods `isLocked` and `isHiddenOnFormulaBar` to `Cell`. I corrected the docs to explain this. And, as long as I was looking at the docs, I corrected some examples to use `getHighestDataRow/Column` rather than `getHighestRow/Column`, a frequent problem for users (e.g. PHPOffice#3721). As a side note, the change to Cell.php is my first use of the nullsafe operator. This is one of many new options available now that we require Php8.0+.
1 parent bd633b1 commit 68632aa

File tree

7 files changed

+228
-13
lines changed

7 files changed

+228
-13
lines changed

docs/topics/accessing-cells.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -479,8 +479,8 @@ $spreadsheet = $reader->load("test.xlsx");
479479

480480
$worksheet = $spreadsheet->getActiveSheet();
481481
// Get the highest row and column numbers referenced in the worksheet
482-
$highestRow = $worksheet->getHighestRow(); // e.g. 10
483-
$highestColumn = $worksheet->getHighestColumn(); // e.g 'F'
482+
$highestRow = $worksheet->getHighestDataRow(); // e.g. 10
483+
$highestColumn = $worksheet->getHighestDataColumn(); // e.g 'F'
484484
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); // e.g. 5
485485

486486
echo '<table>' . "\n";
@@ -505,8 +505,8 @@ $spreadsheet = $reader->load("test.xlsx");
505505

506506
$worksheet = $spreadsheet->getActiveSheet();
507507
// Get the highest row number and column letter referenced in the worksheet
508-
$highestRow = $worksheet->getHighestRow(); // e.g. 10
509-
$highestColumn = $worksheet->getHighestColumn(); // e.g 'F'
508+
$highestRow = $worksheet->getHighestDataRow(); // e.g. 10
509+
$highestColumn = $worksheet->getHighestDataColumn(); // e.g 'F'
510510
// Increment the highest column letter
511511
$highestColumn++;
512512

docs/topics/recipes.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -1309,12 +1309,17 @@ when setting a new password.
13091309

13101310
### Cell
13111311

1312-
An example on setting cell security:
1312+
An example on setting cell security.
1313+
Note that cell security is honored only when sheet is protected.
1314+
Also note that the `hidden` property applies only to formulas,
1315+
and tells whether the formula is hidden on the formula bar,
1316+
not in the cell.
13131317

13141318
```php
13151319
$spreadsheet->getActiveSheet()->getStyle('B1')
13161320
->getProtection()
1317-
->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED);
1321+
->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED)
1322+
->setHidden(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_PROTECTED);
13181323
```
13191324

13201325
## Reading protected spreadsheet

src/PhpSpreadsheet/Cell/Cell.php

+26
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1212
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
1313
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
14+
use PhpOffice\PhpSpreadsheet\Style\Protection;
1415
use PhpOffice\PhpSpreadsheet\Style\Style;
1516
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
1617
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -805,4 +806,29 @@ public function getIgnoredErrors(): IgnoredErrors
805806
{
806807
return $this->ignoredErrors;
807808
}
809+
810+
public function isLocked(): bool
811+
{
812+
$sheet = $this->parent?->getParent();
813+
if ($sheet === null || $sheet->getProtection()->getSheet() !== true) {
814+
return false;
815+
}
816+
$locked = $this->getStyle()->getProtection()->getLocked();
817+
818+
return $locked !== Protection::PROTECTION_UNPROTECTED;
819+
}
820+
821+
public function isHiddenOnFormulaBar(): bool
822+
{
823+
if ($this->getDataType() !== DataType::TYPE_FORMULA) {
824+
return false;
825+
}
826+
$sheet = $this->parent?->getParent();
827+
if ($sheet === null || $sheet->getProtection()->getSheet() !== true) {
828+
return false;
829+
}
830+
$hidden = $this->getStyle()->getProtection()->getHidden();
831+
832+
return $hidden !== Protection::PROTECTION_UNPROTECTED;
833+
}
808834
}

src/PhpSpreadsheet/Reader/Xlsx.php

+10-7
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public function listWorksheetInfo($filename)
236236
$relTarget = (string) $rel['Target'];
237237
$dir = dirname($relTarget);
238238
$namespace = dirname($relType);
239-
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', '');
239+
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
240240

241241
$worksheets = [];
242242
foreach ($relsWorkbook->Relationship as $elex) {
@@ -556,7 +556,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
556556
$dir = dirname($relTarget);
557557

558558
// Do not specify namespace in next stmt - do it in Xpath
559-
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', '');
559+
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
560560
$relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
561561

562562
$worksheets = [];
@@ -1342,9 +1342,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
13421342
$drawingFilename = substr($drawingFilename, 5);
13431343
}
13441344
if ($zip->locateName($drawingFilename) !== false) {
1345-
$relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS);
1345+
$relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
13461346
$drawings = [];
1347-
foreach ($relsWorksheet->Relationship as $ele) {
1347+
foreach ($relsWorksheet->Relationship as $elex) {
1348+
$ele = self::getAttributes($elex);
13481349
if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
13491350
$eleTarget = (string) $ele['Target'];
13501351
if (substr($eleTarget, 0, 4) === '/xl/') {
@@ -1362,12 +1363,13 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
13621363
$drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
13631364
$fileDrawing = $drawings[$drawingRelId];
13641365
$drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
1365-
$relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase);
1366+
$relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
13661367

13671368
$images = [];
13681369
$hyperlinks = [];
13691370
if ($relsDrawing && $relsDrawing->Relationship) {
1370-
foreach ($relsDrawing->Relationship as $ele) {
1371+
foreach ($relsDrawing->Relationship as $elex) {
1372+
$ele = self::getAttributes($elex);
13711373
$eleType = (string) $ele['Type'];
13721374
if ($eleType === Namespaces::HYPERLINK) {
13731375
$hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
@@ -1607,7 +1609,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
16071609

16081610
// store original rId of drawing files
16091611
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
1610-
foreach ($relsWorksheet->Relationship as $ele) {
1612+
foreach ($relsWorksheet->Relationship as $elex) {
1613+
$ele = self::getAttributes($elex);
16111614
if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
16121615
$drawingRelId = (string) $ele['Id'];
16131616
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
8+
9+
class Issue3720Test extends \PHPUnit\Framework\TestCase
10+
{
11+
private static string $testbook = 'tests/data/Reader/XLSX/issue.3720.xlsx';
12+
13+
public function testPreliminaries(): void
14+
{
15+
$file = 'zip://';
16+
$file .= self::$testbook;
17+
$file .= '#xl/_rels/workbook.xml.rels';
18+
$data = file_get_contents($file);
19+
// confirm that file contains expected namespaced xml tag
20+
if ($data === false) {
21+
self::fail('Unable to read file');
22+
} else {
23+
self::assertStringContainsString('<ns3:Relationships ', $data);
24+
}
25+
}
26+
27+
public function testInfo(): void
28+
{
29+
$reader = new Xlsx();
30+
$workSheetInfo = $reader->listWorkSheetInfo(self::$testbook);
31+
$info1 = $workSheetInfo[1];
32+
self::assertEquals('Welcome', $info1['worksheetName']);
33+
self::assertEquals('H', $info1['lastColumnLetter']);
34+
self::assertEquals(7, $info1['lastColumnIndex']);
35+
self::assertEquals(49, $info1['totalRows']);
36+
self::assertEquals(8, $info1['totalColumns']);
37+
}
38+
39+
public function testSheetNames(): void
40+
{
41+
$reader = new Xlsx();
42+
$worksheetNames = $reader->listWorksheetNames(self::$testbook);
43+
$expected = [
44+
'Data',
45+
'Welcome',
46+
'Sheet 1',
47+
'Sheet 2',
48+
'Sheet 3',
49+
'Sheet 4',
50+
'Sheet 5',
51+
'Sheet 6',
52+
'Sheet 7',
53+
'Sheet 8',
54+
'Sheet 9',
55+
'Sheet 10',
56+
];
57+
self::assertEquals($expected, $worksheetNames);
58+
}
59+
60+
public function testLoadXlsx(): void
61+
{
62+
$reader = new Xlsx();
63+
$spreadsheet = $reader->load(self::$testbook);
64+
$sheets = $spreadsheet->getAllSheets();
65+
self::assertCount(12, $sheets);
66+
$sheet = $spreadsheet->getSheetByNameOrThrow('Sheet 1');
67+
$sheetProtection = $sheet->getProtection();
68+
self::assertTrue($sheetProtection->getSheet());
69+
self::assertSame(' FILL IN WHITE CELLS ONLY', $sheet->getCell('B3')->getValue());
70+
// inherit because no cell, row, or column style.
71+
// effectively protected because sheet is locked.
72+
self::assertTrue($sheet->getCell('A12')->isLocked());
73+
// unprotected because column is unprotected (no cell or row style)
74+
self::assertFalse($sheet->getCell('B12')->isLocked());
75+
// inherit because cell has style with protection omitted.
76+
// effectively protected because sheet is locked.
77+
self::assertTrue($sheet->getCell('B11')->isLocked());
78+
$sheet = $spreadsheet->getSheetByNameOrThrow('Welcome');
79+
$drawings = $sheet->getDrawingCollection();
80+
self::assertCount(1, $drawings);
81+
$failmsg = '';
82+
if (isset($drawings[0])) {
83+
$draw0 = $drawings[0];
84+
if (method_exists($draw0, 'getPath')) {
85+
self::assertSame('image1.jpeg', basename($draw0->getPath()));
86+
} else {
87+
$failmsg = 'unexpected missing getPath method';
88+
}
89+
} else {
90+
$failmsg = 'unexpected missing array item 0';
91+
}
92+
$spreadsheet->disconnectWorksheets();
93+
if ($failmsg !== '') {
94+
self::fail($failmsg);
95+
}
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheet\Style\Protection;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class Protection2Test extends TestCase
12+
{
13+
public function testisHiddenOnFormulaBar(): void
14+
{
15+
$spreadsheet = new Spreadsheet();
16+
$sheet = $spreadsheet->getActiveSheet();
17+
$sheet->getCell('A1')->setValue('X')
18+
->getStyle()->getProtection()
19+
->setHidden(Protection::PROTECTION_UNPROTECTED);
20+
$sheet->getCell('A2')->setValue('=SUM(1,2)')
21+
->getStyle()->getProtection()
22+
->setHidden(Protection::PROTECTION_UNPROTECTED);
23+
$sheet->getCell('B1')->setValue('X')
24+
->getStyle()->getProtection()
25+
->setHidden(Protection::PROTECTION_PROTECTED);
26+
$sheet->getCell('B2')->setValue('=SUM(1,2)')
27+
->getStyle()->getProtection()
28+
->setHidden(Protection::PROTECTION_PROTECTED);
29+
$sheet->getCell('C1')->setValue('X');
30+
$sheet->getCell('C2')->setValue('=SUM(1,2)');
31+
self::assertFalse($sheet->getCell('A1')->isHiddenOnFormulaBar());
32+
self::assertFalse($sheet->getCell('A2')->isHiddenOnFormulaBar());
33+
self::assertFalse($sheet->getCell('B1')->isHiddenOnFormulaBar());
34+
self::assertFalse($sheet->getCell('B2')->isHiddenOnFormulaBar());
35+
self::assertFalse($sheet->getCell('C1')->isHiddenOnFormulaBar());
36+
self::assertFalse($sheet->getCell('C2')->isHiddenOnFormulaBar());
37+
$sheetProtection = $sheet->getProtection();
38+
$sheetProtection->setSheet(true);
39+
self::assertFalse($sheet->getCell('A1')->isHiddenOnFormulaBar());
40+
self::assertFalse($sheet->getCell('A2')->isHiddenOnFormulaBar());
41+
self::assertFalse($sheet->getCell('B1')->isHiddenOnFormulaBar(), 'not a formula1');
42+
self::assertTrue($sheet->getCell('B2')->isHiddenOnFormulaBar());
43+
self::assertFalse($sheet->getCell('C1')->isHiddenOnFormulaBar(), 'not a formula2');
44+
self::assertTrue($sheet->getCell('C2')->isHiddenOnFormulaBar());
45+
self::assertFalse($sheet->getCell('D1')->isHiddenOnFormulaBar(), 'uninitialized cell is not formula');
46+
$spreadsheet->disconnectWorksheets();
47+
}
48+
49+
public function testisLocked(): void
50+
{
51+
$spreadsheet = new Spreadsheet();
52+
$sheet = $spreadsheet->getActiveSheet();
53+
$sheet->getCell('A1')->setValue('X')
54+
->getStyle()->getProtection()
55+
->setLocked(Protection::PROTECTION_UNPROTECTED);
56+
$sheet->getCell('A2')->setValue('=SUM(1,2)')
57+
->getStyle()->getProtection()
58+
->setLocked(Protection::PROTECTION_UNPROTECTED);
59+
$sheet->getCell('B1')->setValue('X')
60+
->getStyle()->getProtection()
61+
->setLocked(Protection::PROTECTION_PROTECTED);
62+
$sheet->getCell('B2')->setValue('=SUM(1,2)')
63+
->getStyle()->getProtection()
64+
->setLocked(Protection::PROTECTION_PROTECTED);
65+
$sheet->getCell('C1')->setValue('X');
66+
$sheet->getCell('C2')->setValue('=SUM(1,2)');
67+
self::assertFalse($sheet->getCell('A1')->isLocked());
68+
self::assertFalse($sheet->getCell('A2')->isLocked());
69+
self::assertFalse($sheet->getCell('B1')->isLocked());
70+
self::assertFalse($sheet->getCell('B2')->isLocked());
71+
self::assertFalse($sheet->getCell('C1')->isLocked());
72+
self::assertFalse($sheet->getCell('C2')->isLocked());
73+
$sheetProtection = $sheet->getProtection();
74+
$sheetProtection->setSheet(true);
75+
self::assertFalse($sheet->getCell('A1')->isLocked());
76+
self::assertFalse($sheet->getCell('A2')->isLocked());
77+
self::assertTrue($sheet->getCell('B1')->isLocked());
78+
self::assertTrue($sheet->getCell('B2')->isLocked());
79+
self::assertTrue($sheet->getCell('C1')->isLocked());
80+
self::assertTrue($sheet->getCell('C2')->isLocked());
81+
self::assertTrue($sheet->getCell('D1')->isLocked(), 'uninitialized cell');
82+
$spreadsheet->disconnectWorksheets();
83+
}
84+
}
99.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)