From cadb4523085eab13b3b63072c9b663ffe95cb5a7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 3 Aug 2023 08:27:06 -0700 Subject: [PATCH 1/5] Xlsx Reader Namespacing for Tables, AutoFilters Fix #3665. The original issue was the use of an absolute path in the rels file pointing to the comments file. That was easy to take care of, but a bigger problem with the spreadsheet accompanying the problem report was that it used unexpected spacing for AutoFilters and Tables. AutoFilters were already known not to be covered, but Tables appeared after the namespacing changes, but without namespacing support. This PR fixes the absolute path problem and adds namespacing support for Tables and AutoFilters. Remaining areas which are still namespace unaware, mainly because of the absence of test samples which use them with unexpected namespacing, include conditional formatting (internal or external), sheet view options, sheet protection, unparsed loaded data, data validation (internal or external), alternate content, and header/footer images. --- src/PhpSpreadsheet/Reader/Xlsx.php | 27 ++++-- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 79 ++++++++++-------- .../Reader/Xlsx/TableReader.php | 51 ++++++----- .../Reader/Xlsx/Issue3665Test.php | 62 ++++++++++++++ tests/data/Reader/XLSX/issue.3665.xlsx | Bin 0 -> 7639 bytes 5 files changed, 157 insertions(+), 62 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3665Test.php create mode 100644 tests/data/Reader/XLSX/issue.3665.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 1c33fd6811..368d4ddeaa 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -793,6 +793,14 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $docSheet->setTitle((string) $eleSheetAttr['name'], false, false); $fileWorksheet = (string) $worksheets[$sheetReferenceId]; + // issue 3665 adds test for /. + // This broke XlsxRootZipFilesTest, + // but Excel reports an error with that file. + // Testing dir for . avoids this problem. + // It might be better just to drop the test. + if ($fileWorksheet[0] == '/' && $dir !== '.') { + $fileWorksheet = substr($fileWorksheet, strlen($dir) + 2); + } $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS); $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS); @@ -980,8 +988,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } if ($this->readDataOnly === false) { - $this->readAutoFilter($xmlSheet, $docSheet); - $this->readTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); + $this->readAutoFilter($xmlSheetNS, $docSheet); + $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS); } if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) { @@ -2206,10 +2214,14 @@ private function readTables( Worksheet $docSheet, string $dir, string $fileWorksheet, - ZipArchive $zip + ZipArchive $zip, + string $namespaceTable ): void { - if ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { - $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); + if ($xmlSheet && $xmlSheet->tableParts) { + $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0]; + if (((int) $attributes['count']) > 0) { + $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable); + } } } @@ -2218,7 +2230,8 @@ private function readTablesInTablesFile( string $dir, string $fileWorksheet, ZipArchive $zip, - Worksheet $docSheet + Worksheet $docSheet, + string $namespaceTable ): void { foreach ($xmlSheet->tableParts->tablePart as $tablePart) { $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT); @@ -2236,7 +2249,7 @@ private function readTablesInTablesFile( $relationshipFilePath = File::realpath($relationshipFilePath); if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { - $tableXml = $this->loadZip($relationshipFilePath); + $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable); (new TableReader($docSheet, $tableXml))->load(); } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index a6ab4d894b..4538aeb84e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; use PhpOffice\PhpSpreadsheet\Worksheet\Table; @@ -32,36 +33,39 @@ public function __construct($parent, SimpleXMLElement $worksheetXml) public function load(): void { // Remove all "$" in the auto filter range - $autoFilterRange = (string) preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref'] ?? ''); + $attrs = $this->worksheetXml->autoFilter->attributes() ?? []; + $autoFilterRange = (string) preg_replace('/\$/', '', $attrs['ref'] ?? ''); if (strpos($autoFilterRange, ':') !== false) { - $this->readAutoFilter($autoFilterRange, $this->worksheetXml); + $this->readAutoFilter($autoFilterRange); } } - private function readAutoFilter(string $autoFilterRange, SimpleXMLElement $xmlSheet): void + private function readAutoFilter(string $autoFilterRange): void { $autoFilter = $this->parent->getAutoFilter(); $autoFilter->setRange($autoFilterRange); - foreach ($xmlSheet->autoFilter->filterColumn as $filterColumn) { - $column = $autoFilter->getColumnByOffset((int) $filterColumn['colId']); + foreach ($this->worksheetXml->autoFilter->filterColumn as $filterColumn) { + $attributes = $filterColumn->attributes() ?? []; + $column = $autoFilter->getColumnByOffset((int) ($attributes['colId'] ?? 0)); // Check for standard filters if ($filterColumn->filters) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); - $filters = $filterColumn->filters; + $filters = Xlsx::testSimpleXml($filterColumn->filters->attributes()); if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Standard filters are always an OR join, so no join rule needs to be set // Entries can be either filter elements - foreach ($filters->filter as $filterRule) { + foreach ($filterColumn->filters->filter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule('', (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $attr2 = $filterRule->attributes() ?? ['val' => '']; + $column->createRule()->setRule('', (string) $attr2['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Or Date Group elements - $this->readDateRangeAutoFilter($filters, $column); + $this->readDateRangeAutoFilter($filterColumn->filters, $column); } // Check for custom filters @@ -76,20 +80,23 @@ private function readAutoFilter(string $autoFilterRange, SimpleXMLElement $xmlSh private function readDateRangeAutoFilter(SimpleXMLElement $filters, Column $column): void { - foreach ($filters->dateGroupItem as $dateGroupItem) { + foreach ($filters->dateGroupItem as $dateGroupItemx) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule( - '', - [ - 'year' => (string) $dateGroupItem['year'], - 'month' => (string) $dateGroupItem['month'], - 'day' => (string) $dateGroupItem['day'], - 'hour' => (string) $dateGroupItem['hour'], - 'minute' => (string) $dateGroupItem['minute'], - 'second' => (string) $dateGroupItem['second'], - ], - (string) $dateGroupItem['dateTimeGrouping'] - )->setRuleType(Rule::AUTOFILTER_RULETYPE_DATEGROUP); + $dateGroupItem = $dateGroupItemx->attributes(); + if ($dateGroupItem !== null) { + $column->createRule()->setRule( + '', + [ + 'year' => (string) $dateGroupItem['year'], + 'month' => (string) $dateGroupItem['month'], + 'day' => (string) $dateGroupItem['day'], + 'hour' => (string) $dateGroupItem['hour'], + 'minute' => (string) $dateGroupItem['minute'], + 'second' => (string) $dateGroupItem['second'], + ], + (string) $dateGroupItem['dateTimeGrouping'] + )->setRuleType(Rule::AUTOFILTER_RULETYPE_DATEGROUP); + } } } @@ -98,15 +105,17 @@ private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $c if (isset($filterColumn, $filterColumn->customFilters)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); $customFilters = $filterColumn->customFilters; + $attributes = $customFilters->attributes(); // Custom filters can an AND or an OR join; // and there should only ever be one or two entries - if ((isset($customFilters['and'])) && ((string) $customFilters['and'] === '1')) { + if ((isset($attributes['and'])) && ((string) $attributes['and'] === '1')) { $column->setJoin(Column::AUTOFILTER_COLUMN_JOIN_AND); } foreach ($customFilters->customFilter as $filterRule) { + $attr2 = $filterRule->attributes() ?? ['operator' => '', 'val' => '']; $column->createRule()->setRule( - (string) $filterRule['operator'], - (string) $filterRule['val'] + (string) $attr2['operator'], + (string) $attr2['val'] )->setRuleType(Rule::AUTOFILTER_RULETYPE_CUSTOMFILTER); } } @@ -119,16 +128,17 @@ private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $ // We should only ever have one dynamic filter foreach ($filterColumn->dynamicFilter as $filterRule) { // Operator is undefined, but always treated as EQUAL + $attr2 = $filterRule->attributes() ?? []; $column->createRule()->setRule( '', - (string) $filterRule['val'], - (string) $filterRule['type'] + (string) ($attr2['val'] ?? ''), + (string) ($attr2['type'] ?? '') )->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); - if (isset($filterRule['val'])) { - $column->setAttribute('val', (string) $filterRule['val']); + if (isset($attr2['val'])) { + $column->setAttribute('val', (string) $attr2['val']); } - if (isset($filterRule['maxVal'])) { - $column->setAttribute('maxVal', (string) $filterRule['maxVal']); + if (isset($attr2['maxVal'])) { + $column->setAttribute('maxVal', (string) $attr2['maxVal']); } } } @@ -140,15 +150,16 @@ private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $c $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); // We should only ever have one top10 filter foreach ($filterColumn->top10 as $filterRule) { + $attr2 = $filterRule->attributes() ?? []; $column->createRule()->setRule( ( - ((isset($filterRule['percent'])) && ((string) $filterRule['percent'] === '1')) + ((isset($attr2['percent'])) && ((string) $attr2['percent'] === '1')) ? Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT : Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE ), - (string) $filterRule['val'], + (string) ($attr2['val'] ?? ''), ( - ((isset($filterRule['top'])) && ((string) $filterRule['top'] === '1')) + ((isset($attr2['top'])) && ((string) $attr2['top'] === '1')) ? Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP : Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM ) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php index cf89e995f0..09df0dc7e7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -19,6 +19,9 @@ class TableReader */ private $tableXml; + /** @var array|SimpleXMLElement */ + private $tableAttributes; + public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml) { $this->worksheet = $workSheet; @@ -30,28 +33,29 @@ public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml) */ public function load(): void { + $this->tableAttributes = $this->tableXml->attributes() ?? []; // Remove all "$" in the table range - $tableRange = (string) preg_replace('/\$/', '', $this->tableXml['ref'] ?? ''); + $tableRange = (string) preg_replace('/\$/', '', $this->tableAttributes['ref'] ?? ''); if (strpos($tableRange, ':') !== false) { - $this->readTable($tableRange, $this->tableXml); + $this->readTable($tableRange); } } /** * Read Table from xml. */ - private function readTable(string $tableRange, SimpleXMLElement $tableXml): void + private function readTable(string $tableRange): void { $table = new Table($tableRange); - $table->setName((string) $tableXml['displayName']); - $table->setShowHeaderRow((string) $tableXml['headerRowCount'] !== '0'); - $table->setShowTotalsRow((string) $tableXml['totalsRowCount'] === '1'); + $table->setName((string) ($this->tableAttributes['displayName'] ?? '')); + $table->setShowHeaderRow(((string) ($this->tableAttributes['headerRowCount'] ?? '')) !== '0'); + $table->setShowTotalsRow(((string) ($this->tableAttributes['totalsRowCount'] ?? '')) === '1'); - $this->readTableAutoFilter($table, $tableXml->autoFilter); - $this->readTableColumns($table, $tableXml->tableColumns); - $this->readTableStyle($table, $tableXml->tableStyleInfo); + $this->readTableAutoFilter($table, $this->tableXml->autoFilter); + $this->readTableColumns($table, $this->tableXml->tableColumns); + $this->readTableStyle($table, $this->tableXml->tableStyleInfo); - (new AutoFilter($table, $tableXml))->load(); + (new AutoFilter($table, $this->tableXml))->load(); $this->worksheet->addTable($table); } @@ -67,8 +71,9 @@ private function readTableAutoFilter(Table $table, SimpleXMLElement $autoFilterX } foreach ($autoFilterXml->filterColumn as $filterColumn) { - $column = $table->getColumnByOffset((int) $filterColumn['colId']); - $column->setShowFilterButton((string) $filterColumn['hiddenButton'] !== '1'); + $attributes = $filterColumn->attributes() ?? ['colId' => 0, 'hiddenButton' => 0]; + $column = $table->getColumnByOffset((int) $attributes['colId']); + $column->setShowFilterButton(((string) $attributes['hiddenButton']) !== '1'); } } @@ -79,15 +84,16 @@ private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXm { $offset = 0; foreach ($tableColumnsXml->tableColumn as $tableColumn) { + $attributes = $tableColumn->attributes() ?? ['totalsRowLabel' => 0, 'totalsRowFunction' => 0]; $column = $table->getColumnByOffset($offset++); if ($table->getShowTotalsRow()) { - if ($tableColumn['totalsRowLabel']) { - $column->setTotalsRowLabel((string) $tableColumn['totalsRowLabel']); + if ($attributes['totalsRowLabel']) { + $column->setTotalsRowLabel((string) $attributes['totalsRowLabel']); } - if ($tableColumn['totalsRowFunction']) { - $column->setTotalsRowFunction((string) $tableColumn['totalsRowFunction']); + if ($attributes['totalsRowFunction']) { + $column->setTotalsRowFunction((string) $attributes['totalsRowFunction']); } } @@ -103,11 +109,14 @@ private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXm private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void { $tableStyle = new TableStyle(); - $tableStyle->setTheme((string) $tableStyleInfoXml['name']); - $tableStyle->setShowRowStripes((string) $tableStyleInfoXml['showRowStripes'] === '1'); - $tableStyle->setShowColumnStripes((string) $tableStyleInfoXml['showColumnStripes'] === '1'); - $tableStyle->setShowFirstColumn((string) $tableStyleInfoXml['showFirstColumn'] === '1'); - $tableStyle->setShowLastColumn((string) $tableStyleInfoXml['showLastColumn'] === '1'); + $attributes = $tableStyleInfoXml->attributes(); + if ($attributes !== null) { + $tableStyle->setTheme((string) $attributes['name']); + $tableStyle->setShowRowStripes((string) $attributes['showRowStripes'] === '1'); + $tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1'); + $tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1'); + $tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1'); + } $table->setStyle($tableStyle); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3665Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3665Test.php new file mode 100644 index 0000000000..ba1778698e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3665Test.php @@ -0,0 +1,62 @@ +', $data); + self::assertStringContainsString('', $data); + } + + $file = 'zip://'; + $file .= self::$testbook; + $file .= '#xl/worksheets/_rels/sheet1.xml.rels'; + $data = file_get_contents($file); + // confirm absolute path as reference + if ($data === false) { + self::fail('Unable to read rels file'); + } else { + self::assertStringContainsString('Target="/xl/comments1.xml"', $data); + } + } + + public function testTable(): void + { + $reader = new Xlsx(); + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + $tables = $sheet->getTableCollection(); + self::assertCount(1, $tables); + $table = $tables->offsetGet(0); + if ($table === null) { + self::fail('Unexpected failure obtaining table'); + } else { + self::assertEquals('Table5', $table->getName()); + self::assertEquals('A3:M4', $table->getRange()); + self::assertTrue($table->getAllowFilter()); + self::assertSame('A3:M4', $table->getAutoFilter()->getRange()); + } + $comment = $sheet->getComment('A3'); + self::assertSame('Code20', (string) $comment); + $comment = $sheet->getComment('B3'); + self::assertSame('Text100', (string) $comment); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.3665.xlsx b/tests/data/Reader/XLSX/issue.3665.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ad844bdc18463c2022f57b70df9c961e059442ae GIT binary patch literal 7639 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 literal 0 HcmV?d00001 From fe4b418561cea7a3b117e0a453e70d90eece5425 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:09:16 -0700 Subject: [PATCH 2/5] Mysterious Warning for Php7.4 Only Node no longer exists, doesn't affect result. Suppress warning. --- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 4538aeb84e..594bcc5e0d 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -34,7 +34,8 @@ public function load(): void { // Remove all "$" in the auto filter range $attrs = $this->worksheetXml->autoFilter->attributes() ?? []; - $autoFilterRange = (string) preg_replace('/\$/', '', $attrs['ref'] ?? ''); + // Mysterious 'Node no longer exists' warning for Php7.4 only. + $autoFilterRange = (string) @preg_replace('/\$/', '', $attrs['ref'] ?? ''); if (strpos($autoFilterRange, ':') !== false) { $this->readAutoFilter($autoFilterRange); } From 12eeb1a44b4fce54de95cb0b3a371514dfe4c3af Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:36:43 -0700 Subject: [PATCH 3/5] Scrutinizer What would a change be without some new false positives? --- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 6 +++--- src/PhpSpreadsheet/Reader/Xlsx/TableReader.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 594bcc5e0d..8e80e464e1 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -47,7 +47,7 @@ private function readAutoFilter(string $autoFilterRange): void $autoFilter->setRange($autoFilterRange); foreach ($this->worksheetXml->autoFilter->filterColumn as $filterColumn) { - $attributes = $filterColumn->attributes() ?? []; + $attributes = $filterColumn->/** @scrutinizer ignore-call */ attributes() ?? []; $column = $autoFilter->getColumnByOffset((int) ($attributes['colId'] ?? 0)); // Check for standard filters if ($filterColumn->filters) { @@ -61,7 +61,7 @@ private function readAutoFilter(string $autoFilterRange): void // Entries can be either filter elements foreach ($filterColumn->filters->filter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $attr2 = $filterRule->attributes() ?? ['val' => '']; + $attr2 = $filterRule->/** @scrutinizer ignore-call */ attributes() ?? ['val' => '']; $column->createRule()->setRule('', (string) $attr2['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } @@ -83,7 +83,7 @@ private function readDateRangeAutoFilter(SimpleXMLElement $filters, Column $colu { foreach ($filters->dateGroupItem as $dateGroupItemx) { // Operator is undefined, but always treated as EQUAL - $dateGroupItem = $dateGroupItemx->attributes(); + $dateGroupItem = $dateGroupItemx->/** @scrutinizer ignore-call */ attributes(); if ($dateGroupItem !== null) { $column->createRule()->setRule( '', diff --git a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php index 09df0dc7e7..7f8fd85596 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -71,7 +71,7 @@ private function readTableAutoFilter(Table $table, SimpleXMLElement $autoFilterX } foreach ($autoFilterXml->filterColumn as $filterColumn) { - $attributes = $filterColumn->attributes() ?? ['colId' => 0, 'hiddenButton' => 0]; + $attributes = $filterColumn->/** @scrutinizer ignore-call */ attributes() ?? ['colId' => 0, 'hiddenButton' => 0]; $column = $table->getColumnByOffset((int) $attributes['colId']); $column->setShowFilterButton(((string) $attributes['hiddenButton']) !== '1'); } @@ -84,7 +84,7 @@ private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXm { $offset = 0; foreach ($tableColumnsXml->tableColumn as $tableColumn) { - $attributes = $tableColumn->attributes() ?? ['totalsRowLabel' => 0, 'totalsRowFunction' => 0]; + $attributes = $tableColumn->/** @scrutinizer ignore-call */ attributes() ?? ['totalsRowLabel' => 0, 'totalsRowFunction' => 0]; $column = $table->getColumnByOffset($offset++); if ($table->getShowTotalsRow()) { From 57228002b1fff9e7c5fc2dc099ca9ea4a379b318 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:06:46 -0700 Subject: [PATCH 4/5] Scrutinizer A gift that keeps on giving :-( Now it's deciding that things that it didn't report in a scan from a few minutes ago are worth reporting, even with no relevant code changes. --- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 8e80e464e1..eeca0b649c 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -113,7 +113,7 @@ private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $c $column->setJoin(Column::AUTOFILTER_COLUMN_JOIN_AND); } foreach ($customFilters->customFilter as $filterRule) { - $attr2 = $filterRule->attributes() ?? ['operator' => '', 'val' => '']; + $attr2 = $filterRule->/** @scrutinizer ignore-call */ attributes() ?? ['operator' => '', 'val' => '']; $column->createRule()->setRule( (string) $attr2['operator'], (string) $attr2['val'] From 33ef0bde06cf34a93ae8ff64806da0f4e94b9000 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:33:36 -0700 Subject: [PATCH 5/5] Scrutinizer It is an idiot. Let me see if I can fix one false positive and succeed in guessing where it might report another one, even though it didn't do so with this last run. --- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index eeca0b649c..bc406e652a 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -129,7 +129,7 @@ private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $ // We should only ever have one dynamic filter foreach ($filterColumn->dynamicFilter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $attr2 = $filterRule->attributes() ?? []; + $attr2 = $filterRule->/** @scrutinizer ignore-call */ attributes() ?? []; $column->createRule()->setRule( '', (string) ($attr2['val'] ?? ''), @@ -151,7 +151,7 @@ private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $c $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); // We should only ever have one top10 filter foreach ($filterColumn->top10 as $filterRule) { - $attr2 = $filterRule->attributes() ?? []; + $attr2 = $filterRule->/** @scrutinizer ignore-call */ attributes() ?? []; $column->createRule()->setRule( ( ((isset($attr2['percent'])) && ((string) $attr2['percent'] === '1'))