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 00000000..030f8db2 Binary files /dev/null and b/tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic.xlsx differ 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 00000000..24893e74 Binary files /dev/null and b/tests/data/Features/AutoFilter/Xlsx/AutoFilter_Basic_Office365.xlsx differ