From 15170cf8cdfb40b31baf94f76729bf256fb94459 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 12 Jul 2021 03:19:40 +0200 Subject: [PATCH] Issue 2216 resolve office365 auto filter structure move (#2218) * Initial adjustments to Xlsx Reader for two possible locations for AutoFiter information, either on the sheet itself for older files, or in the tables/tableX file for more recent files * Refactor AutoFilter Reader logic into separate methods; preparatory work toward the eventual goal of moving it into its own dedicated AutoFilterTables class * Basic unit tests to verify that the Xlsx Reader can read both the older and Office365 variants of the files used to store AutoFilter structure --- src/PhpSpreadsheet/Reader/Xlsx.php | 75 +++++++++++++++++- .../Worksheet/AutoFilter/Column.php | 5 ++ .../AutoFilter/Xlsx/BasicLoadTest.php | 74 +++++++++++++++++ .../AutoFilter/Xlsx/AutoFilter_Basic.xlsx | Bin 0 -> 9912 bytes .../Xlsx/AutoFilter_Basic_Office365.xlsx | Bin 0 -> 11104 bytes 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Features/AutoFilter/Xlsx/BasicLoadTest.php create mode 100644 tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic.xlsx create mode 100644 tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic_Office365.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index d94a8852..d78b227e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -336,6 +336,29 @@ class Xlsx extends BaseReader } } + /** + * @param string $fileName + */ + private function fileExistsInArchive(ZipArchive $archive, $fileName = ''): bool + { + // Root-relative paths + if (strpos($fileName, '//') !== false) { + $fileName = substr($fileName, strpos($fileName, '//') + 1); + } + $fileName = File::realpath($fileName); + + // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming + // so we need to load case-insensitively from the zip file + + // Apache POI fixes + $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE); + if ($contents === false) { + $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE); + } + + return $contents !== false; + } + /** * @param string $fileName * @@ -821,8 +844,8 @@ class Xlsx extends BaseReader $this->readSheetProtection($docSheet, $xmlSheet); } - if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) { - (new AutoFilter($docSheet, $xmlSheet))->load(); + if ($this->readDataOnly === false) { + $this->readAutoFilterTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); } if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) { @@ -1977,4 +2000,52 @@ class Xlsx extends BaseReader } } } + + private function readAutoFilterTables( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet, + string $dir, + string $fileWorksheet, + ZipArchive $zip + ): void { + if ($xmlSheet && $xmlSheet->autoFilter) { + // In older files, autofilter structure is defined in the worksheet file + (new AutoFilter($docSheet, $xmlSheet))->load(); + } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) { + // But for Office365, MS decided to make it all just a bit more complicated + $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); + } + } + + private function readAutoFilterTablesInTablesFile( + SimpleXMLElement $xmlSheet, + string $dir, + string $fileWorksheet, + ZipArchive $zip, + Worksheet $docSheet + ): void { + foreach ($xmlSheet->tableParts->tablePart as $tablePart) { + $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT); + $tablePartRel = (string) $relation['id']; + $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; + + if ($zip->locateName($relationsFileName)) { + $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS); + foreach ($relsTableReferences->Relationship as $relationship) { + $relationshipAttributes = self::getAttributes($relationship, ''); + + if ((string) $relationshipAttributes['Id'] === $tablePartRel) { + $relationshipFileName = (string) $relationshipAttributes['Target']; + $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName; + $relationshipFilePath = File::realpath($relationshipFilePath); + + if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { + $autoFilter = $this->loadZip($relationshipFilePath); + (new AutoFilter($docSheet, $autoFilter))->load(); + } + } + } + } + } + } } diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php index 6bedceca..3c4bee47 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php @@ -267,6 +267,11 @@ class Column return null; } + public function ruleCount(): int + { + return count($this->ruleset); + } + /** * Get all AutoFilter Column Rules. * diff --git a/tests/PhpSpreadsheetTests/Features/AutoFilter/Xlsx/BasicLoadTest.php b/tests/PhpSpreadsheetTests/Features/AutoFilter/Xlsx/BasicLoadTest.php new file mode 100644 index 00000000..c36e6741 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Features/AutoFilter/Xlsx/BasicLoadTest.php @@ -0,0 +1,74 @@ +load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame('A1:D57', $worksheet->getAutoFilter()->getRange()); + self::assertSame(2, $worksheet->getAutoFilter()->getColumn('C')->ruleCount()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + $worksheet->getAutoFilter()->getColumn('C')->getRules()[0]->getOperator() + ); + self::assertSame('UK', $worksheet->getAutoFilter()->getColumn('C')->getRules()[0]->getValue()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + $worksheet->getAutoFilter()->getColumn('C')->getRules()[1]->getOperator() + ); + self::assertSame('United States', $worksheet->getAutoFilter()->getColumn('C')->getRules()[1]->getValue()); + self::assertSame(2, $worksheet->getAutoFilter()->getColumn('D')->ruleCount()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN, + $worksheet->getAutoFilter()->getColumn('D')->getRules()[0]->getOperator() + ); + self::assertSame('650', $worksheet->getAutoFilter()->getColumn('D')->getRules()[0]->getValue()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, + $worksheet->getAutoFilter()->getColumn('D')->getRules()[1]->getOperator() + ); + self::assertSame('800', $worksheet->getAutoFilter()->getColumn('D')->getRules()[1]->getValue()); + } + + public function testLoadOffice365AutoFilter(): void + { + $filename = 'tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic_Office365.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame('A1:D57', $worksheet->getAutoFilter()->getRange()); + self::assertSame(2, $worksheet->getAutoFilter()->getColumn('C')->ruleCount()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + $worksheet->getAutoFilter()->getColumn('C')->getRules()[0]->getOperator() + ); + self::assertSame('UK', $worksheet->getAutoFilter()->getColumn('C')->getRules()[0]->getValue()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + $worksheet->getAutoFilter()->getColumn('C')->getRules()[1]->getOperator() + ); + self::assertSame('United States', $worksheet->getAutoFilter()->getColumn('C')->getRules()[1]->getValue()); + self::assertSame(2, $worksheet->getAutoFilter()->getColumn('D')->ruleCount()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN, + $worksheet->getAutoFilter()->getColumn('D')->getRules()[0]->getOperator() + ); + self::assertSame('650', $worksheet->getAutoFilter()->getColumn('D')->getRules()[0]->getValue()); + self::assertSame( + Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, + $worksheet->getAutoFilter()->getColumn('D')->getRules()[1]->getOperator() + ); + self::assertSame('800', $worksheet->getAutoFilter()->getColumn('D')->getRules()[1]->getValue()); + } +} diff --git a/tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic.xlsx b/tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..030f8db2fd1378eec70ca1ff6f879aed77db36d5 GIT binary patch literal 9912 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 G`1XIA#cXo` literal 0 HcmV?d00001 diff --git a/tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic_Office365.xlsx b/tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic_Office365.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..24893e7480d12cc5366b57d9c322fb7a2639ad48 GIT binary patch literal 11104 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