Skip to content

Commit c5527ba

Browse files
authored
Unexpected Namespacing in rels File (#3722)
* Unexpected Namespacing in rels File Fix #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 #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. #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+. * Minor Simplifications * Scrutinizer It's being silly again. In many tests, we test a variable for non-null, then use that variable later and Scrutinizer knows it's not null. Not here. Oh well. * Add Methods Test if protected without allocating cell if it doesn't exist.
1 parent 7712d55 commit c5527ba

File tree

8 files changed

+381
-13
lines changed

8 files changed

+381
-13
lines changed

docs/topics/accessing-cells.md

Lines changed: 4 additions & 4 deletions
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 26 additions & 0 deletions
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;
@@ -778,4 +779,29 @@ public function getIgnoredErrors(): IgnoredErrors
778779
{
779780
return $this->ignoredErrors;
780781
}
782+
783+
public function isLocked(): bool
784+
{
785+
$protected = $this->parent?->getParent()?->getProtection()?->getSheet();
786+
if ($protected !== true) {
787+
return false;
788+
}
789+
$locked = $this->getStyle()->getProtection()->getLocked();
790+
791+
return $locked !== Protection::PROTECTION_UNPROTECTED;
792+
}
793+
794+
public function isHiddenOnFormulaBar(): bool
795+
{
796+
if ($this->getDataType() !== DataType::TYPE_FORMULA) {
797+
return false;
798+
}
799+
$protected = $this->parent?->getParent()?->getProtection()?->getSheet();
800+
if ($protected !== true) {
801+
return false;
802+
}
803+
$hidden = $this->getStyle()->getProtection()->getHidden();
804+
805+
return $hidden !== Protection::PROTECTION_UNPROTECTED;
806+
}
781807
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public function listWorksheetInfo(string $filename): array
220220
$relTarget = (string) $rel['Target'];
221221
$dir = dirname($relTarget);
222222
$namespace = dirname($relType);
223-
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', '');
223+
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
224224

225225
$worksheets = [];
226226
foreach ($relsWorkbook->Relationship as $elex) {
@@ -533,7 +533,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
533533
$dir = dirname($relTarget);
534534

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

539539
$worksheets = [];
@@ -1319,9 +1319,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
13191319
$drawingFilename = substr($drawingFilename, 5);
13201320
}
13211321
if ($zip->locateName($drawingFilename) !== false) {
1322-
$relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS);
1322+
$relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
13231323
$drawings = [];
1324-
foreach ($relsWorksheet->Relationship as $ele) {
1324+
foreach ($relsWorksheet->Relationship as $elex) {
1325+
$ele = self::getAttributes($elex);
13251326
if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
13261327
$eleTarget = (string) $ele['Target'];
13271328
if (str_starts_with($eleTarget, '/xl/')) {
@@ -1339,12 +1340,13 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
13391340
$drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
13401341
$fileDrawing = $drawings[$drawingRelId];
13411342
$drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
1342-
$relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase);
1343+
$relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
13431344

13441345
$images = [];
13451346
$hyperlinks = [];
13461347
if ($relsDrawing && $relsDrawing->Relationship) {
1347-
foreach ($relsDrawing->Relationship as $ele) {
1348+
foreach ($relsDrawing->Relationship as $elex) {
1349+
$ele = self::getAttributes($elex);
13481350
$eleType = (string) $ele['Type'];
13491351
if ($eleType === Namespaces::HYPERLINK) {
13501352
$hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
@@ -1584,7 +1586,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
15841586

15851587
// store original rId of drawing files
15861588
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
1587-
foreach ($relsWorksheet->Relationship as $ele) {
1589+
foreach ($relsWorksheet->Relationship as $elex) {
1590+
$ele = self::getAttributes($elex);
15881591
if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
15891592
$drawingRelId = (string) $ele['Id'];
15901593
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PhpOffice\PhpSpreadsheet\Style\Color;
3030
use PhpOffice\PhpSpreadsheet\Style\Conditional;
3131
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
32+
use PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
3233
use PhpOffice\PhpSpreadsheet\Style\Style;
3334

3435
class Worksheet implements IComparable
@@ -1486,6 +1487,11 @@ public function rowDimensionExists(int $row): bool
14861487
return isset($this->rowDimensions[$row]);
14871488
}
14881489

1490+
public function columnDimensionExists(string $column): bool
1491+
{
1492+
return isset($this->columnDimensions[$column]);
1493+
}
1494+
14891495
/**
14901496
* Get column dimension at a specific column.
14911497
*
@@ -3842,4 +3848,53 @@ public function isRowVisible(int $row): bool
38423848
{
38433849
return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
38443850
}
3851+
3852+
/**
3853+
* Same as Cell->isLocked, but without creating cell if it doesn't exist.
3854+
*/
3855+
public function isCellLocked(string $coordinate): bool
3856+
{
3857+
if ($this->getProtection()->getsheet() !== true) {
3858+
return false;
3859+
}
3860+
if ($this->cellExists($coordinate)) {
3861+
return $this->getCell($coordinate)->isLocked();
3862+
}
3863+
$spreadsheet = $this->parent;
3864+
$xfIndex = $this->getXfIndex($coordinate);
3865+
if ($spreadsheet === null || $xfIndex === null) {
3866+
return true;
3867+
}
3868+
3869+
return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
3870+
}
3871+
3872+
/**
3873+
* Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
3874+
*/
3875+
public function isCellHiddenOnFormulaBar(string $coordinate): bool
3876+
{
3877+
if ($this->cellExists($coordinate)) {
3878+
return $this->getCell($coordinate)->isHiddenOnFormulaBar();
3879+
}
3880+
3881+
// cell doesn't exist, therefore isn't a formula,
3882+
// therefore isn't hidden on formula bar.
3883+
return false;
3884+
}
3885+
3886+
private function getXfIndex(string $coordinate): ?int
3887+
{
3888+
[$column, $row] = Coordinate::coordinateFromString($coordinate);
3889+
$row = (int) $row;
3890+
$xfIndex = null;
3891+
if ($this->rowDimensionExists($row)) {
3892+
$xfIndex = $this->getRowDimension($row)->getXfIndex();
3893+
}
3894+
if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
3895+
$xfIndex = $this->getColumnDimension($column)->getXfIndex();
3896+
}
3897+
3898+
return $xfIndex;
3899+
}
38453900
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
8+
use PhpOffice\PhpSpreadsheet\Style\Protection;
9+
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
10+
11+
class Issue3720Test extends \PHPUnit\Framework\TestCase
12+
{
13+
private static string $testbook = 'tests/data/Reader/XLSX/issue.3720.xlsx';
14+
15+
public function testPreliminaries(): void
16+
{
17+
$file = 'zip://';
18+
$file .= self::$testbook;
19+
$file .= '#xl/_rels/workbook.xml.rels';
20+
$data = file_get_contents($file);
21+
// confirm that file contains expected namespaced xml tag
22+
if ($data === false) {
23+
self::fail('Unable to read file');
24+
} else {
25+
self::assertStringContainsString('<ns3:Relationships ', $data);
26+
}
27+
}
28+
29+
public function testInfo(): void
30+
{
31+
$reader = new Xlsx();
32+
$workSheetInfo = $reader->listWorkSheetInfo(self::$testbook);
33+
$info1 = $workSheetInfo[1];
34+
self::assertEquals('Welcome', $info1['worksheetName']);
35+
self::assertEquals('H', $info1['lastColumnLetter']);
36+
self::assertEquals(7, $info1['lastColumnIndex']);
37+
self::assertEquals(49, $info1['totalRows']);
38+
self::assertEquals(8, $info1['totalColumns']);
39+
}
40+
41+
public function testSheetNames(): void
42+
{
43+
$reader = new Xlsx();
44+
$worksheetNames = $reader->listWorksheetNames(self::$testbook);
45+
$expected = [
46+
'Data',
47+
'Welcome',
48+
'Sheet 1',
49+
'Sheet 2',
50+
'Sheet 3',
51+
'Sheet 4',
52+
'Sheet 5',
53+
'Sheet 6',
54+
'Sheet 7',
55+
'Sheet 8',
56+
'Sheet 9',
57+
'Sheet 10',
58+
];
59+
self::assertEquals($expected, $worksheetNames);
60+
}
61+
62+
public function testLoadXlsx(): void
63+
{
64+
$reader = new Xlsx();
65+
$spreadsheet = $reader->load(self::$testbook);
66+
$sheets = $spreadsheet->getAllSheets();
67+
self::assertCount(12, $sheets);
68+
$sheet = $spreadsheet->getSheetByNameOrThrow('Sheet 1');
69+
$sheetProtection = $sheet->getProtection();
70+
self::assertTrue($sheetProtection->getSheet());
71+
self::assertSame(' FILL IN WHITE CELLS ONLY', $sheet->getCell('B3')->getValue());
72+
// inherit because cell style is inherit.
73+
// effectively protected because sheet is locked.
74+
self::assertTrue($sheet->cellExists('A12'));
75+
self::assertSame(Protection::PROTECTION_INHERIT, $sheet->getStyle('A12')->getProtection()->getLocked());
76+
self::assertTrue($sheet->getCell('A12')->isLocked());
77+
// unprotected because column is unprotected (no cell or row dimension style)
78+
self::assertFalse($sheet->cellExists('B12'));
79+
self::assertFalse($sheet->rowDimensionExists(12));
80+
self::assertTrue($sheet->columnDimensionExists('B'));
81+
$dxf = $sheet->getColumnDimension('B')->getXfIndex();
82+
if ($dxf === null) {
83+
self::fail('Unexpected null column xfIndex');
84+
} else {
85+
self::assertSame(Protection::PROTECTION_UNPROTECTED, $spreadsheet->getCellXfByIndex($dxf)->getProtection()->getLocked());
86+
}
87+
self::assertFalse($sheet->getCell('B12')->isLocked());
88+
// inherit because cell doesn't exist, no row dimension, no column dimension.
89+
// effectively protected because sheet is locked.
90+
self::assertFalse($sheet->cellExists('W8'));
91+
self::assertFalse($sheet->rowDimensionExists(8));
92+
self::assertFalse($sheet->columnDimensionExists('W'));
93+
self::assertTrue($sheet->getCell('W8')->isLocked());
94+
// inherit because cell doesn't exist, row dimension exists without style, no column dimension.
95+
// effectively protected because sheet is locked.
96+
self::assertFalse($sheet->cellExists('X11'));
97+
self::assertTrue($sheet->rowDimensionExists(11));
98+
$dxf = $sheet->getRowDimension(11)->getXfIndex();
99+
self::assertNull($dxf);
100+
self::assertFalse($sheet->columnDimensionExists('X'));
101+
self::assertTrue($sheet->getCell('X11')->isLocked());
102+
103+
$sheet = $spreadsheet->getSheetByNameOrThrow('Welcome');
104+
$drawings = $sheet->getDrawingCollection();
105+
self::assertCount(1, $drawings);
106+
$draw0 = $drawings[0] ?? new Drawing();
107+
self::assertSame('Picture 1', $draw0->getName());
108+
self::assertSame('C3', $draw0->getCoordinates());
109+
self::assertSame('C10', $draw0->getCoordinates2());
110+
self::assertSame('oneCell', $draw0->getEditAs());
111+
$spreadsheet->disconnectWorksheets();
112+
}
113+
}

0 commit comments

Comments
 (0)