diff --git a/composer.json b/composer.json index 720039f1..dff99d7e 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "ezyang/htmlpurifier": "^4.13", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", - "markbaker/matrix": "^2.0", + "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0" diff --git a/composer.lock b/composer.lock index 9bfe87d0..7cbf16c6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "475a2da5744e2426d6201ca5d9c56283", + "content-hash": "08bcc40376dc4b219b21d172e40c622d", "packages": [ { "name": "ezyang/htmlpurifier", @@ -184,16 +184,16 @@ }, { "name": "markbaker/matrix", - "version": "2.1.3", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/MarkBaker/PHPMatrix.git", - "reference": "174395a901b5ba0925f1d790fa91bab531074b61" + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/174395a901b5ba0925f1d790fa91bab531074b61", - "reference": "174395a901b5ba0925f1d790fa91bab531074b61", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/c66aefcafb4f6c269510e9ac46b82619a904c576", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576", "shasum": "" }, "require": { @@ -213,25 +213,7 @@ "autoload": { "psr-4": { "Matrix\\": "classes/src/" - }, - "files": [ - "classes/src/Functions/adjoint.php", - "classes/src/Functions/antidiagonal.php", - "classes/src/Functions/cofactors.php", - "classes/src/Functions/determinant.php", - "classes/src/Functions/diagonal.php", - "classes/src/Functions/identity.php", - "classes/src/Functions/inverse.php", - "classes/src/Functions/minors.php", - "classes/src/Functions/trace.php", - "classes/src/Functions/transpose.php", - "classes/src/Operations/add.php", - "classes/src/Operations/directsum.php", - "classes/src/Operations/subtract.php", - "classes/src/Operations/multiply.php", - "classes/src/Operations/divideby.php", - "classes/src/Operations/divideinto.php" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -252,9 +234,9 @@ ], "support": { "issues": "https://github.com/MarkBaker/PHPMatrix/issues", - "source": "https://github.com/MarkBaker/PHPMatrix/tree/2.1.3" + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" }, - "time": "2021-05-25T15:42:17+00:00" + "time": "2021-07-01T19:01:15+00:00" }, { "name": "myclabs/php-enum", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f58e230d..8c689f11 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3490,11 +3490,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Settings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\CodePage\\:\\:\\$pageArray has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/CodePage.php - - message: "#^Parameter \\#1 \\$dateValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:timestampToExcel\\(\\) expects int, float\\|int\\|string given\\.$#" count: 1 @@ -6520,16 +6515,6 @@ parameters: count: 1 path: tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php - - - message: "#^Cannot call method getFormattedValue\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\|null\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/XlsTest.php - - - - message: "#^Cannot call method getCoordinate\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\|null\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/XlsTest.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Reader\\\\Xlsx\\\\AutoFilterTest\\:\\:getWorksheetInstance\\(\\) has no return typehint specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 00d86f00..d78b227e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -312,15 +312,16 @@ class Xlsx extends BaseReader private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType): void { + $attr = $c->f->attributes(); $cellDataType = 'f'; $value = "={$c->f}"; $calculatedValue = self::$castBaseType($c); // Shared formula? - if (isset($c->f['t']) && strtolower((string) $c->f['t']) == 'shared') { - $instance = (string) $c->f['si']; + if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') { + $instance = (string) $attr['si']; - if (!isset($sharedFormulas[(string) $c->f['si']])) { + if (!isset($sharedFormulas[(string) $attr['si']])) { $sharedFormulas[$instance] = ['master' => $r, 'formula' => $value]; } else { $master = Coordinate::indexesFromString($sharedFormulas[$instance]['master']); @@ -335,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 * @@ -820,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) { @@ -1976,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/Shared/CodePage.php b/src/PhpSpreadsheet/Shared/CodePage.php index 1d5d8933..8718a613 100644 --- a/src/PhpSpreadsheet/Shared/CodePage.php +++ b/src/PhpSpreadsheet/Shared/CodePage.php @@ -8,6 +8,7 @@ class CodePage { public const DEFAULT_CODE_PAGE = 'CP1252'; + /** @var array */ private static $pageArray = [ 0 => 'CP1252', // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program 367 => 'ASCII', // ASCII @@ -56,7 +57,7 @@ class CodePage 10010 => 'MACROMANIA', // Macintosh Romania 10017 => 'MACUKRAINE', // Macintosh Ukraine 10021 => 'MACTHAI', // Macintosh Thai - 10029 => 'MACCENTRALEUROPE', // Macintosh Central Europe + 10029 => ['MACCENTRALEUROPE', 'MAC-CENTRALEUROPE'], // Macintosh Central Europe 10079 => 'MACICELAND', // Macintosh Icelandic 10081 => 'MACTURKISH', // Macintosh Turkish 10082 => 'MACCROATIAN', // Macintosh Croatian @@ -65,6 +66,7 @@ class CodePage //32769 => 'unsupported', // ANSI Latin I (BIFF2-BIFF3) 65000 => 'UTF-7', // Unicode (UTF-7) 65001 => 'UTF-8', // Unicode (UTF-8) + 99999 => ['unsupported'], // Unicode (UTF-8) ]; public static function validate(string $codePage): bool @@ -83,7 +85,20 @@ class CodePage public static function numberToName(int $codePage): string { if (array_key_exists($codePage, self::$pageArray)) { - return self::$pageArray[$codePage]; + $value = self::$pageArray[$codePage]; + if (is_array($value)) { + foreach ($value as $encoding) { + if (@iconv('UTF-8', $encoding, ' ') !== false) { + self::$pageArray[$codePage] = $encoding; + + return $encoding; + } + } + + throw new PhpSpreadsheetException("Code page $codePage not implemented on this system."); + } else { + return $value; + } } if ($codePage == 720 || $codePage == 32769) { throw new PhpSpreadsheetException("Code page $codePage not supported."); // OEM Arabic 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/PhpSpreadsheetTests/Reader/XlsTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/XlsTest.php similarity index 61% rename from tests/PhpSpreadsheetTests/Reader/XlsTest.php rename to tests/PhpSpreadsheetTests/Reader/Xls/XlsTest.php index 130374b3..94257694 100644 --- a/tests/PhpSpreadsheetTests/Reader/XlsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/XlsTest.php @@ -1,8 +1,10 @@ load($filename); self::assertEquals('Title', $spreadsheet->getSheet(0)->getCell('A1')->getValue()); + $spreadsheet->disconnectWorksheets(); } /** @@ -42,6 +45,8 @@ class XlsTest extends AbstractFunctional self::assertEquals($row, $newrow); self::assertEquals($sheet->getCell('A1')->getFormattedValue(), $newsheet->getCell('A1')->getFormattedValue()); self::assertEquals($sheet->getCell("$col$row")->getFormattedValue(), $newsheet->getCell("$col$row")->getFormattedValue()); + $spreadsheet->disconnectWorksheets(); + $newspreadsheet->disconnectWorksheets(); } /** @@ -71,11 +76,49 @@ class XlsTest extends AbstractFunctional $rowIterator = $sheet->getRowIterator(); foreach ($rowIterator as $row) { - foreach ($row->getCellIterator() as $cell) { + foreach ($row->getCellIterator() as $cellx) { + /** @var Cell */ + $cell = $cellx; $valOld = $cell->getFormattedValue(); $valNew = $newsheet->getCell($cell->getCoordinate())->getFormattedValue(); self::assertEquals($valOld, $valNew); } } + $spreadsheet->disconnectWorksheets(); + $newspreadsheet->disconnectWorksheets(); + } + + /** + * Test load Xls file with MACCENTRALEUROPE encoding, which is implemented + * as MAC-CENTRALEUROPE on some systems. Issue #549. + */ + public function testLoadMacCentralEurope(): void + { + $codePages = CodePage::getEncodings(); + self::assertIsArray($codePages[10029]); + $filename = 'tests/data/Reader/XLS/maccentraleurope.xls'; + $reader = new Xls(); + // When no fix applied, spreadsheet fails to load on some systems + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Ładowność', $sheet->getCell('I1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + /** + * First test changes array entry in CodePage. + * This test confirms new that new entry is okay. + */ + public function testLoadMacCentralEurope2(): void + { + $codePages = CodePage::getEncodings(); + self::assertIsString($codePages[10029]); + $filename = 'tests/data/Reader/XLS/maccentraleurope.xls'; + $reader = new Xls(); + // When no fix applied, spreadsheet fails to load on some systems + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Ładowność', $sheet->getCell('I1')->getValue()); + $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/SharedFormulaTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/SharedFormulaTest.php new file mode 100644 index 00000000..94f8e349 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/SharedFormulaTest.php @@ -0,0 +1,41 @@ +', $data); + self::assertStringContainsString('', $data); + } + } + + public function testLoadSheetsXlsxChart(): void + { + $filename = self::$testbook; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename, IReader::LOAD_WITH_CHARTS); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=(RANDBETWEEN(-50,250)+100)*10', $sheet->getCell('D6')->getValue()); + self::assertSame('=(RANDBETWEEN(-50,250)+100)*10', $sheet->getCell('E6')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/CodePageTest.php b/tests/PhpSpreadsheetTests/Shared/CodePageTest.php index e7dc7469..af036f4e 100644 --- a/tests/PhpSpreadsheetTests/Shared/CodePageTest.php +++ b/tests/PhpSpreadsheetTests/Shared/CodePageTest.php @@ -16,8 +16,15 @@ class CodePageTest extends TestCase */ public function testCodePageNumberToName($expectedResult, $codePageIndex): void { + if ($expectedResult === 'exception') { + $this->expectException(Exception::class); + } $result = CodePage::numberToName($codePageIndex); - self::assertEquals($expectedResult, $result); + if (is_array($expectedResult)) { + self::assertContains($result, $expectedResult); + } else { + self::assertEquals($expectedResult, $result); + } } public function providerCodePage(): array 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 diff --git a/tests/data/Reader/XLS/maccentraleurope.xls b/tests/data/Reader/XLS/maccentraleurope.xls new file mode 100644 index 00000000..98607556 Binary files /dev/null and b/tests/data/Reader/XLS/maccentraleurope.xls differ diff --git a/tests/data/Shared/CodePage.php b/tests/data/Shared/CodePage.php index 82bb23e4..ccb59f24 100644 --- a/tests/data/Shared/CodePage.php +++ b/tests/data/Shared/CodePage.php @@ -233,7 +233,7 @@ return [ ], // Macintosh Central Europe [ - 'MACCENTRALEUROPE', + ['MACCENTRALEUROPE', 'MAC-CENTRALEUROPE'], 10029, ], // Macintosh Icelandic @@ -271,4 +271,9 @@ return [ 'UTF-8', 65001, ], + // invalid + [ + 'exception', + 99999, + ], ];