From dbba80255562892845249f9e9e9c7c014eda547f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 23 May 2023 07:35:03 -0700 Subject: [PATCH 01/89] WIP Contingency for Zipstream 3.0 (#3410) Zipstream's newest production release requires Php8.1. This is not necessarily a problem for us as we are locked to an earlier release. However, psr/simple-cache became a real mess for us as people wanted to install PhpSpreadsheet alongside other products which required a version of simple-cache that could not run in some of the versions of Php which we support, and I would like to avoid having to go through that again. This ticket is a contingency to put us ahead of the curve in case such a problem should arise with Zipstream. There is reason to believe that Zipstream is not so widely used as simple-cache, so we do not need to rush this change in. However, should that belief prove incorrect, the change should be ready to go when needed. --- .php-cs-fixer.dist.php | 1 + composer.json | 2 +- phpstan.neon.dist | 4 ++++ src/PhpSpreadsheet/Writer/Ods.php | 7 +------ src/PhpSpreadsheet/Writer/Xlsx.php | 7 +------ src/PhpSpreadsheet/Writer/ZipStream0.php | 17 +++++++++++++++++ src/PhpSpreadsheet/Writer/ZipStream2.php | 21 +++++++++++++++++++++ src/PhpSpreadsheet/Writer/ZipStream3.php | 22 ++++++++++++++++++++++ 8 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 src/PhpSpreadsheet/Writer/ZipStream0.php create mode 100644 src/PhpSpreadsheet/Writer/ZipStream2.php create mode 100644 src/PhpSpreadsheet/Writer/ZipStream3.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 4179c6771e..db70727185 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,6 +2,7 @@ $finder = PhpCsFixer\Finder::create() ->exclude('vendor') + ->notPath('src/PhpSpreadsheet/Writer/ZipStream3.php') ->in(__DIR__); $config = new PhpCsFixer\Config(); diff --git a/composer.json b/composer.json index 3678c8eb62..1d6f34792e 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "ext-zip": "*", "ext-zlib": "*", "ezyang/htmlpurifier": "^4.15", - "maennchen/zipstream-php": "^2.1", + "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 30bd6c2f7b..ef2ae14fac 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,6 +12,10 @@ parameters: excludePaths: - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php + - src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php + - src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php + - src/PhpSpreadsheet/Writer/ZipStream2.php + - src/PhpSpreadsheet/Writer/ZipStream3.php parallel: processTimeout: 300.0 checkMissingIterableValueType: false diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 872be52dee..c9e0ba839f 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -12,7 +12,6 @@ use PhpOffice\PhpSpreadsheet\Writer\Ods\Styles; use PhpOffice\PhpSpreadsheet\Writer\Ods\Thumbnails; use ZipStream\Exception\OverflowException; -use ZipStream\Option\Archive; use ZipStream\ZipStream; class Ods extends BaseWriter @@ -158,11 +157,7 @@ private function createZip() } // Create new ZIP stream - $options = new Archive(); - $options->setEnableZip64(false); - $options->setOutputStream($this->fileHandle); - - return new ZipStream(null, $options); + return ZipStream0::newZipStream($this->fileHandle); } /** diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 3f677b0563..6ed12d4aa1 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -31,7 +31,6 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet; use ZipArchive; use ZipStream\Exception\OverflowException; -use ZipStream\Option\Archive; use ZipStream\ZipStream; class Xlsx extends BaseWriter @@ -546,11 +545,7 @@ public function save($filename, int $flags = 0): void $this->openFileHandle($filename); - $options = new Archive(); - $options->setEnableZip64(false); - $options->setOutputStream($this->fileHandle); - - $this->zip = new ZipStream(null, $options); + $this->zip = ZipStream0::newZipStream($this->fileHandle); $this->addZipFiles($zipContent); diff --git a/src/PhpSpreadsheet/Writer/ZipStream0.php b/src/PhpSpreadsheet/Writer/ZipStream0.php new file mode 100644 index 0000000000..886731ca9e --- /dev/null +++ b/src/PhpSpreadsheet/Writer/ZipStream0.php @@ -0,0 +1,17 @@ +setEnableZip64(false); + $options->setOutputStream($fileHandle); + + return new ZipStream(null, $options); + } +} diff --git a/src/PhpSpreadsheet/Writer/ZipStream3.php b/src/PhpSpreadsheet/Writer/ZipStream3.php new file mode 100644 index 0000000000..d9c8d0b166 --- /dev/null +++ b/src/PhpSpreadsheet/Writer/ZipStream3.php @@ -0,0 +1,22 @@ + Date: Tue, 23 May 2023 08:49:34 -0700 Subject: [PATCH 02/89] Changes to NUMBERVALUE, VALUE, DATEVALUE, TIMEVALUE (#3575) * Changes to NUMBERVALUE, VALUE, DATEVALUE, TIMEVALUE Fix #3574. Reporter received deprecation notice for NUMBERVALUE function with invalid arguments. In fact, the arguments turn out to be valid after all; NUMBERVALUE treats a null-string or an all-blank-string in the first argument as if it were 0. Fixed this, and added several test cases suggested by it. VALUE had been parsing its argument the same way as NUMBERVALUE. However, VALUE does not substitute 0 for null-string or all-blank-string. Coded up the difference between the two, and added the same tests for VALUE as for NUMBERVALUE. VALUE can also pass its argument to DATEVALUE or TIMEVALUE. It is currently over-permissive about that, because Php is over-permissive, e.g. `new DateTime('q')` will return a DateTime object with the current date and time with a timezone of 'q'. Excel will, naturally, return `#VALUE!` for `DATEVALUE('q')`. I don't know that we can ever match Excel's (AFAIK not formally documented) decisions here 100%, but we can get a lot closer by parsing the date string if and only if it contains at least one digit. Code to enforce that is added to DATEVALUE and TIMEVALUE, and appropriate tests are added. * Failure for Php 7.4 Linux After not seeing any such problem for many months, this is the second day in a row where result is different on Windows than Linux with no apparent reason to think why that should be the case. At any rate, easily solved. --- .../Calculation/DateTimeExcel/DateValue.php | 5 +++++ .../Calculation/DateTimeExcel/TimeValue.php | 5 +++++ .../Calculation/TextData/Format.php | 17 +++++++++++++---- .../Functions/DateTime/DateValueTest.php | 9 +++++++++ .../Functions/DateTime/TimeValueTest.php | 9 +++++++++ tests/data/Calculation/DateTime/DATEVALUE.php | 1 + tests/data/Calculation/DateTime/TIMEVALUE.php | 1 + tests/data/Calculation/TextData/NUMBERVALUE.php | 4 ++++ tests/data/Calculation/TextData/VALUE.php | 4 ++++ 9 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index 9a9870d91d..1d59988c3c 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -45,6 +45,11 @@ public static function fromString($dateValue) return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); } + // try to parse as date iff there is at least one digit + if (is_string($dateValue) && preg_match('/\\d/', $dateValue) !== 1) { + return ExcelError::VALUE(); + } + $dti = new DateTimeImmutable(); $baseYear = SharedDateHelper::getExcelCalendar(); $dateValue = trim($dateValue ?? '', '"'); diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php index bb9036f782..78d67b837d 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -42,6 +42,11 @@ public static function fromString($timeValue) return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); } + // try to parse as time iff there is at least one digit + if (is_string($timeValue) && preg_match('/\\d/', $timeValue) !== 1) { + return ExcelError::VALUE(); + } + $timeValue = trim($timeValue ?? '', '"'); $timeValue = str_replace(['/', '.'], '-', $timeValue); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index 5e0762fd69..06433e4b13 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -140,7 +140,7 @@ public static function TEXTFORMAT($value, $format) * * @return mixed */ - private static function convertValue($value) + private static function convertValue($value, bool $spacesMeanZero = false) { $value = $value ?? 0; if (is_bool($value)) { @@ -150,6 +150,12 @@ private static function convertValue($value) throw new CalcExp(ExcelError::VALUE()); } } + if (is_string($value)) { + $value = trim($value); + if ($spacesMeanZero && $value === '') { + $value = 0; + } + } return $value; } @@ -181,6 +187,9 @@ public static function VALUE($value = '') '', trim($value, " \t\n\r\0\x0B" . StringHelper::getCurrencyCode()) ); + if ($numberValue === '') { + return ExcelError::VALUE(); + } if (is_numeric($numberValue)) { return (float) $numberValue; } @@ -277,7 +286,7 @@ public static function NUMBERVALUE($value = '', $decimalSeparator = null, $group } try { - $value = self::convertValue($value); + $value = self::convertValue($value, true); $decimalSeparator = self::getDecimalSeparator($decimalSeparator); $groupSeparator = self::getGroupSeparator($groupSeparator); } catch (CalcExp $e) { @@ -289,8 +298,8 @@ public static function NUMBERVALUE($value = '', $decimalSeparator = null, $group if ($decimalPositions > 1) { return ExcelError::VALUE(); } - $decimalOffset = array_pop($matches[0])[1]; // @phpstan-ignore-line - if (strpos($value, $groupSeparator, $decimalOffset) !== false) { + $decimalOffset = array_pop($matches[0])[1] ?? null; + if ($decimalOffset === null || strpos($value, $groupSeparator, $decimalOffset) !== false) { return ExcelError::VALUE(); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php index 7b704eb62a..9c5abb4105 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php @@ -122,6 +122,15 @@ public static function providerDATEVALUE(): array return require 'tests/data/Calculation/DateTime/DATEVALUE.php'; } + public function testRefArgNull(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('=DATEVALUE(B1)'); + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } + /** * @dataProvider providerUnhappyDATEVALUE */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php index 15d79d482c..b3f5ee5cf5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php @@ -86,6 +86,15 @@ public static function providerTIMEVALUE(): array return require 'tests/data/Calculation/DateTime/TIMEVALUE.php'; } + public function testRefArgNull(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('=TIMEVALUE(B1)'); + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } + public function testTIMEVALUEtoUnixTimestamp(): void { Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); diff --git a/tests/data/Calculation/DateTime/DATEVALUE.php b/tests/data/Calculation/DateTime/DATEVALUE.php index be129cd0d3..508a06c1e7 100644 --- a/tests/data/Calculation/DateTime/DATEVALUE.php +++ b/tests/data/Calculation/DateTime/DATEVALUE.php @@ -91,4 +91,5 @@ [36751, '0-08-13'], [ExcelError::VALUE(), false], [ExcelError::VALUE(), true], + 'do not try to parse if no digits' => [ExcelError::VALUE(), 'x'], ]; diff --git a/tests/data/Calculation/DateTime/TIMEVALUE.php b/tests/data/Calculation/DateTime/TIMEVALUE.php index 3a55e3bfc6..44f4212a9b 100644 --- a/tests/data/Calculation/DateTime/TIMEVALUE.php +++ b/tests/data/Calculation/DateTime/TIMEVALUE.php @@ -18,4 +18,5 @@ [ExcelError::VALUE(), '13:01PM'], [ExcelError::VALUE(), false], [ExcelError::VALUE(), true], + 'do not try to parse if no digits' => [ExcelError::VALUE(), 'x'], ]; diff --git a/tests/data/Calculation/TextData/NUMBERVALUE.php b/tests/data/Calculation/TextData/NUMBERVALUE.php index 791e61cf85..e72194067f 100644 --- a/tests/data/Calculation/TextData/NUMBERVALUE.php +++ b/tests/data/Calculation/TextData/NUMBERVALUE.php @@ -51,4 +51,8 @@ ], 'no arguments' => ['exception'], 'boolean argument' => ['#VALUE!', true], + 'issue 3574 null string treated as 0' => [0, '', ',', ' '], + 'issue 3574 one or more spaces treated as 0' => [0, ' ', ',', ' '], + 'issue 3574 non-blank numeric string okay' => [2, ' 2 ', ',', ' '], + 'issue 3574 non-blank non-numeric string invalid' => ['#VALUE!', ' x ', ',', ' '], ]; diff --git a/tests/data/Calculation/TextData/VALUE.php b/tests/data/Calculation/TextData/VALUE.php index 666f12d102..1a007005ec 100644 --- a/tests/data/Calculation/TextData/VALUE.php +++ b/tests/data/Calculation/TextData/VALUE.php @@ -44,4 +44,8 @@ 'no arguments' => ['exception'], 'bool argument' => ['#VALUE!', false], 'null argument' => ['0', null], + 'issue 3574 null string invalid' => ['#VALUE!', ''], + 'issue 3574 blank string invalid' => ['#VALUE!', ' '], + 'issue 3574 non-blank numeric string okay' => [2, ' 2 '], + 'issue 3574 non-blank non-numeric string invalid' => ['#VALUE!', ' x '], ]; From 407479f1a2ad389d01a4bf2f1d7051c992dc50e5 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 23 May 2023 21:14:12 -0700 Subject: [PATCH 03/89] Non-Static Data Provider (#3585) That is deprecated in PhpUnit 10. All existing ones were corrected, but a new one slipped by. --- CHANGELOG.md | 4 ++++ .../Writer/Html/MemoryDrawingOffsetTest.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f0fc823e..35658bb637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Handle REF error as part of range [Issue #3453](https://github.com/PHPOffice/PhpSpreadsheet/issues/3453) [PR #3467](https://github.com/PHPOffice/PhpSpreadsheet/pull/3467) - Handle Absolute Pathnames in Rels File [Issue #3553](https://github.com/PHPOffice/PhpSpreadsheet/issues/3553) [PR #3554](https://github.com/PHPOffice/PhpSpreadsheet/pull/3554) - Return Page Breaks in Order [Issue #3552](https://github.com/PHPOffice/PhpSpreadsheet/issues/3552) [PR #3555](https://github.com/PHPOffice/PhpSpreadsheet/pull/3555) +- Add position attribute for MemoryDrawing in Html [Issue #3529](https://github.com/PHPOffice/PhpSpreadsheet/issues/3529 [PR #3535](https://github.com/PHPOffice/PhpSpreadsheet/pull/3535) +- Allow Index_number as Array for VLOOKUP/HLOOKUP [Issue #3561](https://github.com/PHPOffice/PhpSpreadsheet/issues/3561 [PR #3570](https://github.com/PHPOffice/PhpSpreadsheet/pull/3570) +- Add Unsupported Options in Xml Spreadsheet [Issue #3566](https://github.com/PHPOffice/PhpSpreadsheet/issues/3566 [Issue #3568](https://github.com/PHPOffice/PhpSpreadsheet/issues/3568 [Issue #3569](https://github.com/PHPOffice/PhpSpreadsheet/issues/3569 [PR #3567](https://github.com/PHPOffice/PhpSpreadsheet/pull/3567) +- Changes to NUMBERVALUE, VALUE, DATEVALUE, TIMEVALUE [Issue #3574](https://github.com/PHPOffice/PhpSpreadsheet/issues/3574 [PR #3575](https://github.com/PHPOffice/PhpSpreadsheet/pull/3575) ## 1.28.0 - 2023-02-25 diff --git a/tests/PhpSpreadsheetTests/Writer/Html/MemoryDrawingOffsetTest.php b/tests/PhpSpreadsheetTests/Writer/Html/MemoryDrawingOffsetTest.php index 2cb2373c98..e367f174df 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/MemoryDrawingOffsetTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/MemoryDrawingOffsetTest.php @@ -36,7 +36,7 @@ public function testMemoryDrawingOffset(int $w, int $h, int $x, int $y): void unset($spreadsheet); } - public function dataProvider(): array + public static function dataProvider(): array { return [ [33, 19, 0, 20], From 9a13f526c53abb9cac00fc5a644bcfcd966e644d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 25 May 2023 13:05:55 -0700 Subject: [PATCH 04/89] Redo Calculation of Color Tinting (#3580) * Redo Calculation of Color Tinting Fix #3550. Some colors are specified in Excel by specifying a theme color to which a tint is applied. The original PHPExcel algorithm for doing this was developed by trial and error, and is good enough a lot of the time. However, for the issue at hand, the resulting color is detectably different from the calculation that Excel makes. Searching the web, I found https://gist.github.com/Mike-Honey/b36e651e9a7f1d2e1d60ce1c63b9b633 which comes much closer for the case in hand, and for all the other cases that I've looked at. That code depends on Python colorsys package; I have adapted the code from the Python gist and package into a new Php class. This doesn't agree perfectly with Excel. However, if each of the red, green, and blue components (each a value between 0 and 255 inclusive) agree within plus or minus 3 (arbitrary choice) of Excel's result, I think that is good enough. I have added a new test member which reads from a spreadsheet with Xml altered by hand to set up several theme/tint cells. These tests use the plus-or-minus-3 criterion. They result in 100% code coverage of the new class. Unsuprisingly, some existing tests failed with the new code. Issue2387Test reads a theme/tint font color, and is changed to use the plus-or-minus-3 criterion, comparing against the color as Excel shows it. ColorChangeBrightness showed 9 failures with the new code. It consists of calculations not involving a spreadsheet. For that reason, I felt it was sufficient to just do an exact match test, changing the 9 old results for new results confirmed with the Python code. I also added one new test case, the one that kicked off this entire PR. * Scrutinizer Being Stupid It strikes again. --- src/PhpSpreadsheet/Style/Color.php | 17 +- src/PhpSpreadsheet/Style/RgbTint.php | 175 ++++++++++++++++++ .../Reader/Xlsx/Issue2387Test.php | 6 +- .../Reader/Xlsx/RgbTintTest.php | 50 +++++ tests/data/Reader/XLSX/RgbTint.xlsx | Bin 0 -> 11025 bytes .../Style/Color/ColorChangeBrightness.php | 23 ++- 6 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 src/PhpSpreadsheet/Style/RgbTint.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/RgbTintTest.php create mode 100644 tests/data/Reader/XLSX/RgbTint.xlsx diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index 3c002b2705..282defc0ce 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -362,23 +362,8 @@ public static function changeBrightness($hexColourValue, $adjustPercentage) $green = self::getGreen($hexColourValue, false); /** @var int $blue */ $blue = self::getBlue($hexColourValue, false); - if ($adjustPercentage > 0) { - $red += (255 - $red) * $adjustPercentage; - $green += (255 - $green) * $adjustPercentage; - $blue += (255 - $blue) * $adjustPercentage; - } else { - $red += $red * $adjustPercentage; - $green += $green * $adjustPercentage; - $blue += $blue * $adjustPercentage; - } - - $rgb = strtoupper( - str_pad(dechex((int) $red), 2, '0', 0) . - str_pad(dechex((int) $green), 2, '0', 0) . - str_pad(dechex((int) $blue), 2, '0', 0) - ); - return (($rgba) ? 'FF' : '') . $rgb; + return (($rgba) ? 'FF' : '') . RgbTint::rgbAndTintToRgb($red, $green, $blue, $adjustPercentage); } /** diff --git a/src/PhpSpreadsheet/Style/RgbTint.php b/src/PhpSpreadsheet/Style/RgbTint.php new file mode 100644 index 0000000000..582ae48397 --- /dev/null +++ b/src/PhpSpreadsheet/Style/RgbTint.php @@ -0,0 +1,175 @@ += 0.0) ? $hue : (1.0 + $hue); + } + + /** + * Convert red/green/blue to HLSMAX-based hue/luminance/saturation. + * + * @return int[] + */ + private static function rgbToMsHls(int $red, int $green, int $blue): array + { + $red01 = $red / self::RGBMAX; + $green01 = $green / self::RGBMAX; + $blue01 = $blue / self::RGBMAX; + [$hue, $luminance, $saturation] = self::rgbToHls($red01, $green01, $blue01); + + return [ + (int) round($hue * self::HLSMAX), + (int) round($luminance * self::HLSMAX), + (int) round($saturation * self::HLSMAX), + ]; + } + + /** + * Converts HLSMAX based HLS values to rgb values in the range (0,1). + * + * @return float[] + */ + private static function msHlsToRgb(int $hue, int $lightness, int $saturation): array + { + return self::hlsToRgb($hue / self::HLSMAX, $lightness / self::HLSMAX, $saturation / self::HLSMAX); + } + + /** + * Tints HLSMAX based luminance. + * + * @see http://ciintelligence.blogspot.co.uk/2012/02/converting-excel-theme-color-and-tint.html + */ + private static function tintLuminance(float $tint, float $luminance): int + { + if ($tint < 0) { + return (int) round($luminance * (1.0 + $tint)); + } + + return (int) round($luminance * (1.0 - $tint) + (self::HLSMAX - self::HLSMAX * (1.0 - $tint))); + } + + /** + * Return result of tinting supplied rgb as 6 hex digits. + */ + public static function rgbAndTintToRgb(int $red, int $green, int $blue, float $tint): string + { + [$hue, $luminance, $saturation] = self::rgbToMsHls($red, $green, $blue); + [$red, $green, $blue] = self::msHlsToRgb($hue, self::tintLuminance($tint, $luminance), $saturation); + + return sprintf( + '%02X%02X%02X', + (int) round($red * self::RGBMAX), + (int) round($green * self::RGBMAX), + (int) round($blue * self::RGBMAX) + ); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php index 870ea6ab04..7e59418adf 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2387Test.php @@ -15,7 +15,11 @@ public function testIssue2387(): void $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); $sheet = $spreadsheet->getActiveSheet(); - self::assertSame('335593', $sheet->getCell('B2')->getStyle()->getFont()->getColor()->getRgb()); + // Font color being tested uses theme color with tint. + // Excel shows final color as 305496. + $expectedColor = '305496'; + $calculatedColor = $sheet->getCell('B2')->getStyle()->getFont()->getColor()->getRgb(); + self::assertSame($expectedColor, RgbTintTest::compareColors($calculatedColor, $expectedColor)); self::assertSame(Fill::FILL_NONE, $sheet->getCell('B2')->getStyle()->getFill()->getFillType()); self::assertSame('FFFFFF', $sheet->getCell('C2')->getStyle()->getFont()->getColor()->getRgb()); self::assertSame('000000', $sheet->getCell('C2')->getStyle()->getFill()->getStartColor()->getRgb()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RgbTintTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RgbTintTest.php new file mode 100644 index 0000000000..96e2003bcf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RgbTintTest.php @@ -0,0 +1,50 @@ + $maxDiff) { + return $style; + } + if (abs($styleGreen - $textGreen) > $maxDiff) { + return $style; + } + if (abs($styleBlue - $textBlue) > $maxDiff) { + return $style; + } + + return $text; + } + + public function testRgbTint(): void + { + $filename = 'tests/data/Reader/XLSX/RgbTint.xlsx'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $row = 0; + while (true) { + ++$row; + $text = (string) $sheet->getCell("B$row"); + if ($text === '') { + break; + } + $style = $sheet->getStyle("A$row")->getFill()->getStartColor()->getRgb(); + self::assertSame($text, self::compareColors($style, $text), "row $row"); + } + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/RgbTint.xlsx b/tests/data/Reader/XLSX/RgbTint.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0ef26da69fd3594067cb47a9b5170e75e7aaa853 GIT binary patch literal 11025 zcmeHt^(epf> z!#VF?@O}0Vdxj5tU7y+OUe~?Wwbrew2t*(NAOTPS000fZ8}gCH3l0DXL<9iv0Vwd# zr5qi=AP2Cqrl%9g)sWM}-i|sS0iO9a03P=I|E~YT9w*1uI^KC(?@Gb?Zb zoj;NrB@7vmZ1tI|0!K;&Oz6U?YP^vkVGL!#aW-S#P_qmmYn61@E21EcF$IZ9IPEc^ z9FFXkxj7sE5DglSSJHf~GVS!$#c+s#Pb!2lP;DuSaq+W8NlqVs{2#@nTos2(_kO~Fb+$4*sv!&?6}+=U2M%9 z9c}-}aAjKdjs-$Ew|)~h$UW|n!_VPuTF~IY$@5ytTk{nJ?_55hYQwqq@xLp%IT8(x zZ^)cvooQ#THXr?VnVxR-=_=Jw&w5moKUxQ$GMC1%Kx>ds+ny1+64gVpuTLkRoDie~ z&hHqLy>mI##k<*QgnVSlNlnt;X zgG}{NJT2vQi!M+Cg%E=)XV{ByRTd!HHORPi)Ob6B!ljvT?(I!=)i66jV%>ngJ^7koL0Q>KSaclh_doHEW z-1>!uFb8-LeJx8uk+r+7>Di6~HroSD&lapn3a4K%1Z?=?4U0R~o<*Gsu!_C~5VmN?%l45b@@MYQ>7H=hRME_f*y+J(57m|@I2ciE_|VlwSI9sAh=}4D5H|8n zY{ey+3B_Drj+K>mH(Cb|+!VL(E-+&{>Vkzfvvro6f{GS{2o2ITrGN2Qdl)mJ`E#UM z{%Fu<=s?kI8GY%7`jhBkUOEW-_L!9>J5_}PRmzctLag`NBb4MqRxkXnBbM)X{>q5l zgq_rV(HHEA!U(&fL``Sj{%Q>afjm7$5A%o#Y9nrK zdLsmD+7oc~8|;PN3-I-kc!uVBXh(Ahy2UT?@UE!sIcU88Jgh(c@3m_z;G>48;RQ`Glag=_z3-iQfB@*P@=&A03dYd0vN6@>9~R@Tl5J`SwXn^;dJ4 zZ%Jz(#I7-88ER3TKSa5W6Rn=~;cl}e7|KFv#-aBON}bHAkeUd14Gjo5@~+7f#Y(^= z$VOla4feHomA6gekTm-h>Z(Nul6R1@e5WXv1O(=A}W>L)?O`lV2ND6=RMfn zxLX~JQwL}jeagjK)QM44|H{`uiKNt1*ne7JDUJ+)0te&kuPpah*8V5s!NH1CSUUW_ zeU+&{Q|#f!X-B^e<95$}80vmj15wNl%O3adSb zsr*?WuABMkty)K4z)!9RSvnR@*oV%JEH?)Fr&nMr{m(7lH+J0@3%13dAp-y;uqXc9 z;$5vlAh0Xf&kvqI_WCS{Ufeu4LC6Z*`6FFFl>>He9ERvnxGB%l;-OY&hwzZpsfU>* zw-+LLj~R@{NhGS5#N zyHsL6pu?Et59;pvmJNS6ih~ZhQthyn+2-W$rRrp{Y;5IRlYI$SO{9J~3FwtOUb8QV z0ojuJP$gzLgX{8&@~nu{O>dOtQ5|CVV&Z`5S+?&RA2DcjT_%H%?r4-C_~_o_{;QbD zer1IgU{H~u?sv)AOSJvmAtF&VolC)IE+V;N!v$IIISYO;kUvcnHk6s%G~@9cgbeT& z`F|@blezq|Ojt9fdC9=Ch1zSOjRgl~SzWy)ryKpe@)-UCM;b30>QfOj_Z@kq3lCFH zkYXnx+PgYTC%&djoT%A2c<>Rb%XC_D84fK2d?^sFV4ui+PC?z zqPHsOPGW`p;r(;WNw>s*EbY(rO`3d;7R-%`qXyXDJv`3~YS-(iEs(XI zSh2gS7;G`@G6`usrhFPwKcv*+##nzgw*27rvpeLbXjPOd#H;0T)2F>c2C}Mas;k(L zFgQw}6R9Nn<3Qv{Zq+`3fh;rPY*6JRt`KCW_{4t$q zFmWaA&v1aODLVq>UUOop3#>%FD4_{WA1s9eR~V!F<+|u{ ziTWuhgO}5Y*oxH4N$4P#MnPt(-oCsvbO)Hp3jW^j+El*qBs{^4W?s=LsbO1TZ8`d{Tuy`hKVAY5*BdsB#4qBQf)l5Rh5=^X$s{G zskd=&o+q^E+KI(q#t4>dhp>#Q#bcHpsB0+c(h6?sM}Os&00h9R)61_7^kS%tq0}@* zF$=HuEHF%>;5mlkF`6`KvS2D`T~xFHlgXeWQt}{#NnD{D?8iG!X0h817Ma z5BHNE$Yb0cx3^!1KEfM4r{3#&S#{X8K%v`**e2FtJJ&*W+)N*vUMX9RxM&{jM&d5A zT9PsJ?VD4M(o*7Sb3|*zSc2_WPK7eS#Wvq-oi+8jm@3c~W+Yxju~6rPMn|<&G-YY4 zRQTqMIPaQLRm9m>oXX(vNd_qw69#q8Iw`a8p)L^htND}RW&6tRx3<&bJ{EMQu;0P$ z7yfRuZ^)Wc?Lb{vnn!^0eU!xpuZw@@Q^pD|dTFmnmQ$!X`zysQk+5?z$qqD(b*$?L zhCL4G%^Y9!ScjDadV>5;!+fNwpC*$ufhu!wqTekl&9%(X&eRBKqoM=)~sdSle%6yexkB z9)Z-yhLrkyFiQ{yRS)^vzZUEfdsPJ?f8isF}p^} zm93h~S~A|DBSNk`SPWvMQWc7K^u>>s%cu7Jb~N4fQjwVFCP_?{EXmbglBgMDJs~i% zdSx`1t~(Ca9ar5;&C{24-bCZJCnWUP{JtvFz8NMtFBxh{k+f}2_@*wsExH12Bwf}k zk?&nPoT?cYA9Wm9W3I*7)8^%il6ILw4!US?Z4CDWcM$OxcG2OKYrVdf^sp&jm1eVM zau>e67;Icy8Cs^MKt@K5u^v%}t9l!hMpin0+bjGXlI|G~q-MIQ=fwFMO>U&}IS!ZZ zrYWSvHDS_8A_j@dgGf9HY2Haa9L5~}$(a1(6#4dSIsrHm`s*2w*{Cv|aloPNXbO}UMd>U6_qasn6%jnG8=M+mT0e){+SrVk0 z9&|-|v1L`<%eprOPTUO>-M}n8k(#PuEpoU~=5VR7P`ZJ~~j9R)*W>fxpROIacD;|iunAG1H{zkJNTuaJES9b9aiwnTOh6!UWO z7kZ<dk z<8u@n29RHY-dp5k1<|*S1*UMeMmM0 zTZooHiBm$5K95GfYs=}kbaaguub2^V+T>%ri}s2@0$1YioN93dzI;<*u?Qhl+FZ7= zk#`#3c}7@kuz*#Lrr~ggGQ`?}P}p^Hn)lFEZCZDn5~N8f$_~`JI_b_qk8sC$+YlS) zIwg_^R%2697TI_AW6e+;4_D^^%;16KT~EZ9dKB2R;^YE;h|x1u*0D39OX|Q zzl27gmk<~TFcTwJP^UlEr0P83BR2LdzMJUtoA#Ri5%Yi>7-g=zuf>@bFDJRTx*>wz zPq?dNU@Gel*#j|DFpGY-)FE%N!`^2_v#N|q+2Ro+o0TRd*K5vz)BVi$F_#u%#Vy%= z`BgXAAFEP7ZnFG3ZrVE{E|%QjeI9N!QAY2r4Vge2G;%Mvme>zMUrlx8wt?(0i`YU= zq+t}FLhue3n%ystBeJH5a@XVUzQ=XcN7)Ug8DVxg+Q+xlVKv+MwFRO}ihf+Ws1WLg#s%5h1}5V!x8DJ+{jOfSoc)>-5a2@h06t zgY*woP%;#7f@t$b3(AkML>4M;^l1E2qwaw>TIuwU-KMu3qgb4?m=FQb%jFm+!`g!P zk3vQF;o0MJ=c*nnSa!o}rD3vV;j(4mqI(U@nZ2sX#T*JWOT)&1R?L0yQ#crsWOkvG zmtXj@bc9Bz;HrKXS8`|2FX!&|?l{ch6X{vF6ifb6Ag^_RpXybBUR^=Tto~7wLyc&% zus2){BFk~$yVld(cRaC=5`rGEGH;nqWBTHh1k1%Z|4>uRnOssU)S@D{rvDOw1u5=( zG?U3ZXJl1Dx{x`0CXSaTUp1Ez=wCnQP}}2nSn8Oledo^)T}nj_&s^ssN6H1dptw={ zi_WW?^X~4tm(z^pxTq|S`B4cqqp_%*C=!}cOR(?3MDeGss zL>WF+nsv0%CmI#Xq#61&^4vEvIG$Ko`*Rl{-H^Inr z9c5*%s2yfYGKt5z5|<(6j#i*e z<%JJnkR5jC7;9`m^F%L8T2k($-k4CJ^AxaEEQ4Zw`Eya`ml|5ipNj^cC$*%UTXt|vPbsF%zkE2#T<>sEem z*skNq<=;-bqVh|h-FJkjJ+ERX%mGhej-dPHh(DS&f9mgF-J9Pk{AcgxRa|Fe7dL^- z1@KnN!#nYF2!@n~AZ?S@HqZ<@kF<~)e?f5LT}uJ_u(|0py7@5kTI?OUB~}9aR118_ zhB>iy)i>{XjtUCoqh zLPhhY6Hvy9AVnsn@m=D;j?Y)lb&AUR>Ama2?t;7Qlfv07uBHd8gduCVtlU=>mY(OX zK|Z3Fg;i0Pu~JjVSR!{25i$co)!>Lm>l*C>Qr-13+)0Zi3r(vEKF6`fbb)c!8b^yW zrIjjn@H#UlA7S&2S{ig*5@iSjelj>w)->zllsV#yh|#J&7~aD)1;agxOvF;4=yw?P;QBLow#yv7a02`WH&CZ zUWur@tjQ7}%@1IvMhl1kmyb{RA2?u({Ld^-<`m;63k!o6u-+vutaoYQXs+tw=;X>} z?&t#gbDsXcmL<$}(U4A+ZfcU=BgR{7%nPFV^1k>6#i@fj8GI}LWt{i+Jx@?yFWq{- zXq?^?OLGdmxIA7aj+|4T9%s^JA~($Er(uR4CZ{lEvzT>Pc{k);f$U(s4nW2YF*#FRFBDU8IMR021kq>q(8tw+Wn4rUk~S)rP9J@Bm&8Wi zBU$tB;eTU6_Bq@GUK3H`nqnH(Df^e;GnGfhiGT$kF072i`D^f*IywC>_Fyji^U8uq zIxcWygd72(R2WO-!b_?cfn{cLI%id;aF7P8XT|1uHT2$sV&yhJJ*I5Y+l(p2rNa0c*0xKI)ki_!ce!h))s9 zq!p92)${H1_?k?Ft0sCDN!K18f$YevzC4^y#eyFwt!YbfqDB#zO5G)3F7DA%p~5UL zpuGI%?5>1SHr<%iRvUVm0)0~}Y-acN(ywYYIV&o1x531?rl5c`IjhmeHYQ8}dB4vC zfnfz{@4=_4x)^8D`n?y^!9{84V1EyLo%~CC9HcCTGe%MT@M@%mspxtYIlV`{KcYDR zc@Vp_P;=pn5!(wHuBSla?LHb4c+)Qw&f6b>w>A$MCq;XPgpWURA1ezj1UibijtCm1 zt3a8&DO)YN5<)c2yF;veyg zi`}lDb)TPjsrhkU8Cvl%tlZYzqW@d?hHoZL48y|r@YiXizwDhaUBQm_|BK-N#4i9) zm!ufA0Gnf6gxn$*wD&=1duyC|l^y_r*}U;}Q4T@uhR-mnE$kTVnQxAguhCe*>yVho zaumMXm(ZjO;|wX6po+@&Od7AJs_@F*8m%*=&??^^JO*FYk+;F_+)`FM<2OGJGf#?6 zIly_L5)pV6xp}@Q_6f!9%TfiC?;0CESDip-1JGIMEHd`YseFPL z9=YtiF>*#TXNZ{r8s0g3JJ5Y?`@!I+Y=>{dCuOW-)~t_8%l&*?Q3=&>)dymgCii4E zh=b}h>|8LZt{W7J_a?*o#`E;7#`_Jc&_Rg@<|`9jO$Bo-z0l-342}5D)*yb8*5{&H zlplI7RPu!Ca<9jRrQBcfwjT_g2`>1nEBC28P?u>2uqnvSB4F>1ao{565=8sApbAha z-e8y>RTeD$NShd0M9_>z^rd>b&H5au?VN$DgfYxR!?V@oWblH8Z;;7=#0?*PavWbB z*`sA-e#?gyEaS`wiftYstO#A{B$GeXhutJyCxYn>*PNduVK$Bx+#tqXBKdL8=e9G0D{VxY6T5(EU)xz>-IK`jbCcAmEk8od4{YH?dxbK61Raf@`@5__lfQpnq0q=>Ee-$hDLH}Ot z{)Pkq)M1nTf0VxW&HrAm{MCGv<}c=d7BBa$|DG-WY8^}e7i;tX<_=XwL|732(Okm> NSiyLZ^oL9X{6BIneOmwk literal 0 HcmV?d00001 diff --git a/tests/data/Style/Color/ColorChangeBrightness.php b/tests/data/Style/Color/ColorChangeBrightness.php index 8ddf188dab..1c552e157d 100644 --- a/tests/data/Style/Color/ColorChangeBrightness.php +++ b/tests/data/Style/Color/ColorChangeBrightness.php @@ -9,7 +9,7 @@ ], // RGBA [ - 'FF99A8B7', + 'FF92A8BE', 'FFAABBCC', -0.1, ], @@ -20,17 +20,17 @@ 0.1, ], [ - '99A8B7', + '92A8BE', 'AABBCC', -0.1, ], [ - 'FF1919', + 'FF1A1A', 'FF0000', 0.1, ], [ - 'E50000', + 'E60000', 'FF0000', -0.1, ], @@ -40,7 +40,7 @@ 0.1, ], [ - 'E57373', + 'FF5959', 'FF8080', -0.1, ], @@ -50,7 +50,7 @@ 0.15, ], [ - 'D80000', + 'D90000', 'FF0000', -0.15, ], @@ -60,18 +60,23 @@ 0.15, ], [ - 'D86C6C', + 'FF4646', 'FF8080', -0.15, ], [ - 'FFF783', + 'FFF984', 'FFF008', 0.5, ], [ - '7F7804', + '847D00', 'FFF008', -0.5, ], + 'issue 3550' => [ + '558ED5', + '1F497D', + 0.39997558519241921, + ], ]; From 4f6d1f77a33ac6940038b5bb5a2acee6a5657713 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 May 2023 07:16:54 -0700 Subject: [PATCH 05/89] Accommodating Slash with preg_quote - Text Functions (#3582) PR #3513, developed by @SaidkhojaIftikhor, has been stuck for some time awaiting tests. This is the first of three PRs to replace that one. This accomodates the use of slash as a delimiter in functions TEXTAFTER, TEXTBEFORE, TEXTSPLIT, and NUMBERVALUE. The source changes are very simple. Additional tests exercise all the source changes. --- .../Calculation/TextData/Extract.php | 4 ++-- .../Calculation/TextData/Format.php | 2 +- src/PhpSpreadsheet/Calculation/TextData/Text.php | 4 ++-- tests/data/Calculation/TextData/NUMBERVALUE.php | 2 ++ tests/data/Calculation/TextData/TEXTAFTER.php | 7 +++++++ tests/data/Calculation/TextData/TEXTBEFORE.php | 7 +++++++ tests/data/Calculation/TextData/TEXTSPLIT.php | 16 ++++++++++++++++ 7 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 519607c080..24ddff2ec5 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -261,7 +261,7 @@ private static function buildDelimiter($delimiter): string $delimiter = Functions::flattenArray($delimiter); $quotedDelimiters = array_map( function ($delimiter) { - return preg_quote($delimiter ?? ''); + return preg_quote($delimiter ?? '', '/'); }, $delimiter ); @@ -270,7 +270,7 @@ function ($delimiter) { return '(' . $delimiters . ')'; } - return '(' . preg_quote($delimiter ?? '') . ')'; + return '(' . preg_quote($delimiter ?? '', '/') . ')'; } private static function matchFlags(int $matchMode): string diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index 06433e4b13..57d3316637 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -294,7 +294,7 @@ public static function NUMBERVALUE($value = '', $decimalSeparator = null, $group } if (!is_numeric($value)) { - $decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator) . '/', $value, $matches, PREG_OFFSET_CAPTURE); + $decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator, '/') . '/', $value, $matches, PREG_OFFSET_CAPTURE); if ($decimalPositions > 1) { return ExcelError::VALUE(); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 8e6a575a29..b8a730767c 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -193,7 +193,7 @@ private static function buildDelimiter($delimiter): string if (is_array($delimiter) && count($valueSet) > 1) { $quotedDelimiters = array_map( function ($delimiter) { - return preg_quote($delimiter ?? ''); + return preg_quote($delimiter ?? '', '/'); }, $valueSet ); @@ -202,7 +202,7 @@ function ($delimiter) { return '(' . $delimiters . ')'; } - return '(' . preg_quote(/** @scrutinizer ignore-type */ Functions::flattenSingleValue($delimiter)) . ')'; + return '(' . preg_quote(/** @scrutinizer ignore-type */ Functions::flattenSingleValue($delimiter), '/') . ')'; } private static function matchFlags(bool $matchMode): string diff --git a/tests/data/Calculation/TextData/NUMBERVALUE.php b/tests/data/Calculation/TextData/NUMBERVALUE.php index e72194067f..6131317426 100644 --- a/tests/data/Calculation/TextData/NUMBERVALUE.php +++ b/tests/data/Calculation/TextData/NUMBERVALUE.php @@ -51,6 +51,8 @@ ], 'no arguments' => ['exception'], 'boolean argument' => ['#VALUE!', true], + 'slash as group separator' => [1234567.1, '1/234/567.1', '.', '/'], + 'slash as decimal separator' => [1234567.1, '1,234,567/1', '/', ','], 'issue 3574 null string treated as 0' => [0, '', ',', ' '], 'issue 3574 one or more spaces treated as 0' => [0, ' ', ',', ' '], 'issue 3574 non-blank numeric string okay' => [2, ' 2 ', ',', ' '], diff --git a/tests/data/Calculation/TextData/TEXTAFTER.php b/tests/data/Calculation/TextData/TEXTAFTER.php index ebcfecb946..c0e3e3d51f 100644 --- a/tests/data/Calculation/TextData/TEXTAFTER.php +++ b/tests/data/Calculation/TextData/TEXTAFTER.php @@ -248,4 +248,11 @@ 1, ], ], + 'slash delimiter' => [ + 'about/that', + [ + 'How/about/that', + '/', + ], + ], ]; diff --git a/tests/data/Calculation/TextData/TEXTBEFORE.php b/tests/data/Calculation/TextData/TEXTBEFORE.php index 1929354ce5..a79d37b0fd 100644 --- a/tests/data/Calculation/TextData/TEXTBEFORE.php +++ b/tests/data/Calculation/TextData/TEXTBEFORE.php @@ -240,4 +240,11 @@ 1, ], ], + 'slash delimiter' => [ + 'How', + [ + 'How/about/that', + '/', + ], + ], ]; diff --git a/tests/data/Calculation/TextData/TEXTSPLIT.php b/tests/data/Calculation/TextData/TEXTSPLIT.php index 64016ca7a7..a10b26f6e8 100644 --- a/tests/data/Calculation/TextData/TEXTSPLIT.php +++ b/tests/data/Calculation/TextData/TEXTSPLIT.php @@ -104,4 +104,20 @@ '', ], ], + 'slash as column delimiter' => [ + [['Hello', 'World']], + [ + 'Hello/World', + '/', + '', + ], + ], + 'slash as row delimiter' => [ + [['ho', 'w'], ['about', '#N/A'], ['t', 'hat']], + [ + 'ho.w/about/t.hat', + '.', + '/', + ], + ], ]; From 5a6d8b9285b1cd32170ca4585cdda175860b9cf7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 May 2023 07:40:30 -0700 Subject: [PATCH 06/89] Accommodating Slash with preg_quote - Structured Reference Column Names (#3583) PR #3513, developed by @SaidkhojaIftikhor, has been stuck for some time awaiting tests. This is the second of three PRs to replace that one. This accomodates the use of slash as a delimiter in column names in Tables and Structured References. The source changes are very simple. Additional tests exercise all the source changes. There is also a preg_quote call when a table is renamed. I have also changed it to accomodate slash, because it's the right thing to do. But ... I can't think how to test it. PhpSpreadsheet will not allow you to set a table name to a string containing a slash (a test is added to confirm), and, if I manually update the Xml in an Xlsx spreadsheet so that the name does contain a slash, Excel will, understandably, complain that the file is corrupt. --- .../Engine/Operands/StructuredReference.php | 6 +- src/PhpSpreadsheet/Worksheet/Table.php | 4 +- src/PhpSpreadsheet/Worksheet/Table/Column.php | 4 +- .../Engine/StructuredReferenceSlashTest.php | 115 ++++++++++++++++++ .../Worksheet/Table/FormulaTest.php | 26 ++++ .../Worksheet/Table/TableTest.php | 1 + 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Engine/StructuredReferenceSlashTest.php diff --git a/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php b/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php index 266f1b2bb1..59cc3e3d27 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php +++ b/src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php @@ -190,8 +190,8 @@ private function adjustRowReference(string $columnName, string $reference, Cell { if ($columnName !== '') { $cellReference = $columnId . $cell->getRow(); - $pattern1 = '/\[' . preg_quote($columnName) . '\]/miu'; - $pattern2 = '/@' . preg_quote($columnName) . '/miu'; + $pattern1 = '/\[' . preg_quote($columnName, '/') . '\]/miu'; + $pattern2 = '/@' . preg_quote($columnName, '/') . '/miu'; if (preg_match($pattern1, $reference) === 1) { $reference = preg_replace($pattern1, $cellReference, $reference); } elseif (preg_match($pattern2, $reference) === 1) { @@ -328,7 +328,7 @@ private function getColumnsForColumnReference(string $reference, int $startRow, $cellFrom = "{$columnId}{$startRow}"; $cellTo = "{$columnId}{$endRow}"; $cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}"; - $pattern = '/\[' . preg_quote($columnName) . '\]/mui'; + $pattern = '/\[' . preg_quote($columnName, '/') . '\]/mui'; if (preg_match($pattern, $reference) === 1) { $columnsSelected = true; $reference = preg_replace($pattern, $cellReference, $reference); diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index dc2a4f8a52..1bc8dff45f 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -180,7 +180,7 @@ private function updateStructuredReferences(string $name): void private function updateStructuredReferencesInCells(Worksheet $worksheet, string $newName): void { - $pattern = '/' . preg_quote($this->name) . '\[/mui'; + $pattern = '/' . preg_quote($this->name, '/') . '\[/mui'; foreach ($worksheet->getCoordinates(false) as $coordinate) { $cell = $worksheet->getCell($coordinate); @@ -196,7 +196,7 @@ private function updateStructuredReferencesInCells(Worksheet $worksheet, string private function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $newName): void { - $pattern = '/' . preg_quote($this->name) . '\[/mui'; + $pattern = '/' . preg_quote($this->name, '/') . '\[/mui'; foreach ($spreadsheet->getNamedFormulae() as $namedFormula) { $formula = $namedFormula->getValue(); diff --git a/src/PhpSpreadsheet/Worksheet/Table/Column.php b/src/PhpSpreadsheet/Worksheet/Table/Column.php index 30630c0d46..32dd4c4f83 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -225,7 +225,7 @@ public static function updateStructuredReferences(?Worksheet $workSheet, ?string private static function updateStructuredReferencesInCells(Worksheet $worksheet, string $oldTitle, string $newTitle): void { - $pattern = '/\[(@?)' . preg_quote($oldTitle) . '\]/mui'; + $pattern = '/\[(@?)' . preg_quote($oldTitle, '/') . '\]/mui'; foreach ($worksheet->getCoordinates(false) as $coordinate) { $cell = $worksheet->getCell($coordinate); @@ -241,7 +241,7 @@ private static function updateStructuredReferencesInCells(Worksheet $worksheet, private static function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $oldTitle, string $newTitle): void { - $pattern = '/\[(@?)' . preg_quote($oldTitle) . '\]/mui'; + $pattern = '/\[(@?)' . preg_quote($oldTitle, '/') . '\]/mui'; foreach ($spreadsheet->getNamedFormulae() as $namedFormula) { $formula = $namedFormula->getValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/StructuredReferenceSlashTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/StructuredReferenceSlashTest.php new file mode 100644 index 0000000000..8b652999b6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/StructuredReferenceSlashTest.php @@ -0,0 +1,115 @@ +spreadSheet = new Spreadsheet(); + $workSheet = $spreadsheet->getActiveSheet(); + $workSheet->fromArray($this->tableData, null, 'A1'); + + $table = new Table('A1:E8', 'DeptSales'); + $table->setShowTotalsRow(true); + $table->getColumn('A')->setTotalsRowLabel('Total'); + $workSheet->addTable($table); + + return $spreadsheet; + } + + protected function tearDown(): void + { + if ($this->spreadSheet !== null) { + $this->spreadSheet->disconnectWorksheets(); + $this->spreadSheet = null; + } + + parent::tearDown(); + } + + /** + * @dataProvider structuredReferenceProviderColumnData + */ + public function testStructuredReferenceColumns(string $expectedCellRange, string $structuredReference): void + { + $spreadsheet = $this->getSpreadsheet(); + $structuredReferenceObject = new StructuredReference($structuredReference); + $cellRange = $structuredReferenceObject->parse($spreadsheet->getActiveSheet()->getCell('E5')); + self::assertSame($expectedCellRange, $cellRange); + } + + /** + * @dataProvider structuredReferenceProviderRowData + */ + public function testStructuredReferenceRows(string $expectedCellRange, string $structuredReference): void + { + $spreadsheet = $this->getSpreadsheet(); + $structuredReferenceObject = new StructuredReference($structuredReference); + $cellRange = $structuredReferenceObject->parse($spreadsheet->getActiveSheet()->getCell('E5')); + self::assertSame($expectedCellRange, $cellRange); + } + + public static function structuredReferenceProviderColumnData(): array + { + return [ + // Full table, with no column specified, means data only, not headers or totals + 'Full table Unqualified' => ['A2:E7', '[]'], + 'Full table Qualified' => ['A2:E7', 'DeptSales[]'], + // No item identifier, but with a column identifier, means data and header for the column, but no totals + 'Column with no Item Identifier #1' => ['A2:A7', 'DeptSales[[Sales Person]]'], + 'Column with no Item Identifier #2' => ['B2:B7', 'DeptSales[Region]'], + // Item identifier with no column specified + 'Item Identifier only #1' => ['A1:E1', 'DeptSales[#Headers]'], + 'Item Identifier only #2' => ['A1:E1', 'DeptSales[[#Headers]]'], + 'Item Identifier only #3' => ['A8:E8', 'DeptSales[#Totals]'], + 'Item Identifier only #4' => ['A2:E7', 'DeptSales[#Data]'], + // Item identifiers and column identifiers + 'Full column' => ['C1:C8', 'DeptSales[[#All],[Sales Amount]]'], + 'Column Header' => ['D1', 'DeptSales[[#Headers],[% Commission]]'], + 'Column Total' => ['B8', 'DeptSales[[#Totals],[Region]]'], + 'Column Range All' => ['C1:D8', 'DeptSales[[#All],[Sales Amount]:[% Commission]]'], + 'Column Range Data' => ['D2:E7', 'DeptSales[[#Data],[% Commission]:[Commission/Amount]]'], + 'Column Range Headers' => ['B1:E1', 'DeptSales[[#Headers],[Region]:[Commission/Amount]]'], + 'Column Range Totals' => ['C8:E8', 'DeptSales[[#Totals],[Sales Amount]:[Commission/Amount]]'], + 'Column Range Headers and Data' => ['D1:D7', 'DeptSales[[#Headers],[#Data],[% Commission]]'], + 'Column Range No Item Identifier' => ['A2:B7', 'DeptSales[[Sales Person]:[Region]]'], + // ['C2:C7,E2:E7', 'DeptSales[Sales Amount],DeptSales[Commission Amount]'], + // ['B2:C7', 'DeptSales[[Sales Person]:[Sales Amount]] DeptSales[[Region]:[% Commission]]'], + ]; + } + + public static function structuredReferenceProviderRowData(): array + { + return [ + ['E5', 'DeptSales[[#This Row], [Commission/Amount]]'], + ['E5', 'DeptSales[@Commission/Amount]'], + ['E5', 'DeptSales[@[Commission/Amount]]'], + ['C5:D5', 'DeptSales[@[Sales Amount]:[% Commission]]'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/FormulaTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/FormulaTest.php index 69e54446ef..25dc27025a 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/FormulaTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/FormulaTest.php @@ -85,6 +85,32 @@ public function testCellFormulaUpdateOnHeadingColumnChange(): void $spreadsheet->disconnectWorksheets(); } + public function testCellFormulaUpdateOnHeadingColumnChangeSlash(): void + { + $reader = new Xlsx(); + $filename = 'tests/data/Worksheet/Table/TableFormulae.xlsx'; + $spreadsheet = $reader->load($filename); + $worksheet = $spreadsheet->getActiveSheet(); + + // Verify original formulae + // Row Formula + self::assertSame("=DeptSales[[#This Row],[Sales\u{a0}Amount]]*DeptSales[[#This Row],[% Commission]]", $worksheet->getCell('E2')->getValue()); + // Totals Formula + self::assertSame('=SUBTOTAL(109,DeptSales[Commission Amount])', $worksheet->getCell('E8')->getValue()); + + $worksheet->getCell('D1')->setValue('Commission %age'); + $worksheet->getCell('E1')->setValue('Commission/Amount'); + $worksheet->getCell('E1')->setValue('Commission/Amount2'); + + // Verify modified formulae + // Row Formula + self::assertSame("=DeptSales[[#This Row],[Sales\u{a0}Amount]]*DeptSales[[#This Row],[Commission %age]]", $worksheet->getCell('E2')->getValue()); + // Totals Formula + self::assertSame('=SUBTOTAL(109,DeptSales[Commission/Amount2])', $worksheet->getCell('E8')->getValue()); + + $spreadsheet->disconnectWorksheets(); + } + public function testNamedFormulaUpdateOnHeadingColumnChange(): void { $reader = new Xlsx(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index 8c7a4e8eae..5ff0daed12 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -75,6 +75,7 @@ public static function invalidTableNamesProvider(): array ['R11C11'], ['123'], ['=Table'], + ['Name/Slash'], ['ிக'], // starting with UTF-8 combined character [bin2hex(random_bytes(255))], // random string with length greater than 255 ]; From b8191244d51711e66ba5f2439c474475abd73905 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 May 2023 07:54:12 -0700 Subject: [PATCH 07/89] Accommodating Slash with preg_quote - Delimiters (#3584) PR #3513, developed by @SaidkhojaIftikhor, has been stuck for some time awaiting tests. This is the first of three PRs to replace that one. This PR also allows the use of slash as a thousands separator or decimal separator or currency symbol. These are, of course, very unusual situations; the main reason to support them is so that PhpSpreadsheet code will not crash when users set those options. New Calculation/Engine and AdvancedValueBinder tests confirm these. While making these changes, a few errors were found in AdvancedValueBinder and Calculation/Engine/FormattedNumber, e.g. currencies weren't parsed correctly when period was used as the group separator and comma as the decimal separator. Tests have been added for those situations. --- .../Calculation/Engine/FormattedNumber.php | 23 ++-- .../Cell/AdvancedValueBinder.php | 7 +- .../Engine/FormattedNumberSlashTest.php | 104 ++++++++++++++++++ .../Cell/AdvancedValueBinderTest.php | 40 +++---- 4 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php diff --git a/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php b/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php index 3e88ece550..331fa448be 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php +++ b/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php @@ -48,9 +48,9 @@ public static function convertToNumberIfFormatted(string &$operand): bool */ public static function convertToNumberIfNumeric(string &$operand): bool { - $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator()); + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); $value = preg_replace(['/(\d)' . $thousandsSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim($operand)); - $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator()); + $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/'); $value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? ''); if (is_numeric($value)) { @@ -90,9 +90,9 @@ public static function convertToNumberIfFraction(string &$operand): bool */ public static function convertToNumberIfPercent(string &$operand): bool { - $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator()); + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); $value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', trim($operand)); - $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator()); + $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/'); $value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? ''); $match = []; @@ -116,17 +116,22 @@ public static function convertToNumberIfPercent(string &$operand): bool public static function convertToNumberIfCurrency(string &$operand): bool { $currencyRegexp = self::currencyMatcherRegexp(); - $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator()); + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); $value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $operand); $match = []; if ($value !== null && preg_match($currencyRegexp, $value, $match, PREG_UNMATCHED_AS_NULL)) { //Determine the sign $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? ''; + $decimalSeparator = StringHelper::getDecimalSeparator(); //Cast to a float - $operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])); + $intermediate = (string) ($match['PostfixedValue'] ?? $match['PrefixedValue']); + $intermediate = str_replace($decimalSeparator, '.', $intermediate); + if (is_numeric($intermediate)) { + $operand = (float) ($sign . str_replace($decimalSeparator, '.', $intermediate)); - return true; + return true; + } } return false; @@ -134,8 +139,8 @@ public static function convertToNumberIfCurrency(string &$operand): bool public static function currencyMatcherRegexp(): string { - $currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode())); - $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator()); + $currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode(), '/')); + $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/'); return '~^(?:(?: *(?[-+])? *(?[' . $currencyCodes . ']) *(?[-+])? *(?[0-9]+[' . $decimalSeparator . ']?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?[-+])? *(?[0-9]+' . $decimalSeparator . '?[0-9]*(?:E[-+]?[0-9]*)?) *(?[' . $currencyCodes . ']) *))$~ui'; } diff --git a/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php b/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php index 1bf73ba829..c0fb387753 100644 --- a/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php +++ b/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php @@ -51,8 +51,9 @@ public function bindValue(Cell $cell, $value = null) return $this->setImproperFraction($matches, $cell); } - $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator()); - $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator()); + $decimalSeparatorNoPreg = StringHelper::getDecimalSeparator(); + $decimalSeparator = preg_quote($decimalSeparatorNoPreg, '/'); + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); // Check for percentage if (preg_match('/^\-?\d*' . $decimalSeparator . '?\d*\s?\%$/', preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value))) { @@ -64,7 +65,7 @@ public function bindValue(Cell $cell, $value = null) // Convert value to number $sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null; $currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency']; - $value = (float) ($sign . trim(str_replace([$decimalSeparator, $currencyCode, ' ', '-'], ['.', '', '', ''], preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value)))); // @phpstan-ignore-line + $value = (float) ($sign . trim(str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value)))); // @phpstan-ignore-line return $this->setCurrency($value, $cell, $currencyCode); // @phpstan-ignore-line } diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php new file mode 100644 index 0000000000..baaf32f276 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php @@ -0,0 +1,104 @@ +originalCurrencyCode = StringHelper::getCurrencyCode(); + $this->originalDecimalSeparator = StringHelper::getDecimalSeparator(); + $this->originalThousandsSeparator = StringHelper::getThousandsSeparator(); + } + + protected function tearDown(): void + { + StringHelper::setCurrencyCode($this->originalCurrencyCode); + StringHelper::setDecimalSeparator($this->originalDecimalSeparator); + StringHelper::setThousandsSeparator($this->originalThousandsSeparator); + } + + /** + * @dataProvider providerNumbers + * + * @param mixed $expected + */ + public function testNumber($expected, string $value, string $thousandsSeparator = ',', string $decimalSeparator = '.'): void + { + StringHelper::setThousandsSeparator($thousandsSeparator); + StringHelper::setDecimalSeparator($decimalSeparator); + $result = FormattedNumber::convertToNumberIfFormatted($value); + self::assertTrue($result); + self::assertSame($expected, $value); + } + + public static function providerNumbers(): array + { + return [ + 'normal' => [1234.5, '1,234.5'], + 'slash as thousands separator' => [-1234.5, '- 1/234.5', '/', '.'], + 'slash as decimal separator' => [-1234.5, '- 1,234/5', ',', '/'], + ]; + } + + /** + * @dataProvider providerPercentages + */ + public function testPercentage(string $expected, string $value, string $thousandsSeparator = ',', string $decimalSeparator = '.'): void + { + $originalValue = $value; + StringHelper::setThousandsSeparator($thousandsSeparator); + StringHelper::setDecimalSeparator($decimalSeparator); + $result = FormattedNumber::convertToNumberIfPercent($value); + self::assertTrue($result); + self::assertSame($expected, (string) $value); + self::assertNotEquals($value, $originalValue); + } + + public static function providerPercentages(): array + { + return [ + 'normal' => ['21.5034', '2,150.34%'], + 'slash as thousands separator' => ['21.5034', '2/150.34%', '/', '.'], + 'slash as decimal separator' => ['21.5034', '2,150/34%', ',', '/'], + ]; + } + + /** + * @dataProvider providerCurrencies + */ + public function testCurrencies(string $expected, string $value, string $thousandsSeparator = ',', string $decimalSeparator = '.', ?string $currencyCode = null): void + { + $originalValue = $value; + StringHelper::setThousandsSeparator($thousandsSeparator); + StringHelper::setDecimalSeparator($decimalSeparator); + if ($currencyCode !== null) { + StringHelper::setCurrencyCode($currencyCode); + } + $result = FormattedNumber::convertToNumberIfCurrency($value); + self::assertTrue($result); + self::assertSame($expected, (string) $value); + self::assertNotEquals($value, $originalValue); + } + + public static function providerCurrencies(): array + { + return [ + 'switched delimiters' => ['2134.56', '$2.134,56', '.', ','], + 'normal' => ['2134.56', '$2,134.56'], + 'slash as thousands separator' => ['2134.56', '$2/134.56', '/', '.'], + 'slash as decimal separator' => ['2134.56', '$2,134/56', ',', '/'], + 'slash as currency code' => ['2134.56', '/2,134.56', ',', '.', '/'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index 9ba1cf69ba..cb60213e7a 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -14,41 +14,33 @@ class AdvancedValueBinderTest extends TestCase { const AVB_PRECISION = 1.0E-8; - /** - * @var string - */ - private $currencyCode; + private string $originalLocale; - /** - * @var string - */ - private $decimalSeparator; + private string $originalCurrencyCode; - /** - * @var string - */ - private $thousandsSeparator; + private string $originalDecimalSeparator; - /** - * @var IValueBinder - */ - private $valueBinder; + private string $originalThousandsSeparator; + + private IValueBinder $valueBinder; protected function setUp(): void { - Settings::setLocale('en_US'); - $this->currencyCode = StringHelper::getCurrencyCode(); - $this->decimalSeparator = StringHelper::getDecimalSeparator(); - $this->thousandsSeparator = StringHelper::getThousandsSeparator(); + $this->originalLocale = Settings::getLocale(); + $this->originalCurrencyCode = StringHelper::getCurrencyCode(); + $this->originalDecimalSeparator = StringHelper::getDecimalSeparator(); + $this->originalThousandsSeparator = StringHelper::getThousandsSeparator(); + $this->valueBinder = Cell::getValueBinder(); Cell::setValueBinder(new AdvancedValueBinder()); } protected function tearDown(): void { - StringHelper::setCurrencyCode($this->currencyCode); - StringHelper::setDecimalSeparator($this->decimalSeparator); - StringHelper::setThousandsSeparator($this->thousandsSeparator); + StringHelper::setCurrencyCode($this->originalCurrencyCode); + StringHelper::setDecimalSeparator($this->originalDecimalSeparator); + StringHelper::setThousandsSeparator($this->originalThousandsSeparator); + Settings::setLocale($this->originalLocale); Cell::setValueBinder($this->valueBinder); } @@ -134,6 +126,8 @@ public static function currencyProvider(): array ['€2,020.22', 2020.22, ',', '.', '€'], ['$10.11', 10.11, ',', '.', '€'], ['€2,020.20', 2020.2, ',', '.', '$'], + 'slash as group separator' => ['€2/020.20', 2020.2, '/', '.', '$'], + 'slash as decimal separator' => ['€2,020/20', 2020.2, ',', '/', '$'], ['-2,020.20€', -2020.2, ',', '.', '$'], ['- 2,020.20 € ', -2020.2, ',', '.', '$'], ]; From 27b3fc3b229c40a93a5fc4ece361a7ea159dc5a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:06:09 -0700 Subject: [PATCH 08/89] Bump phpunit/phpunit from 9.6.7 to 9.6.8 (#3595) Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 9.6.7 to 9.6.8. - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/9.6.8/ChangeLog-9.6.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/9.6.7...9.6.8) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/composer.lock b/composer.lock index 3e89fec89a..b80350619d 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": "06031132b41198f02eff4f6289d43ae2", + "content-hash": "8c1ccd9ddeaa0f55e5fe86f2d53cc334", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1586,16 +1586,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.15.5", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", + "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", "shasum": "" }, "require": { @@ -1636,9 +1636,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-05-19T20:20:00+00:00" }, { "name": "paragonie/random_compat", @@ -2441,16 +2441,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.7", + "version": "9.6.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", - "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e", "shasum": "" }, "require": { @@ -2524,7 +2524,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.8" }, "funding": [ { @@ -2540,7 +2540,7 @@ "type": "tidelift" } ], - "time": "2023-04-14T08:58:40+00:00" + "time": "2023-05-11T05:14:45+00:00" }, { "name": "psr/cache", @@ -3092,16 +3092,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -3146,7 +3146,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -3154,7 +3154,7 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", From 27e8a1f03db5b83b5a93900ad2e05d6ec2130e8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:16:29 -0700 Subject: [PATCH 09/89] Bump mpdf/mpdf from 8.1.5 to 8.1.6 (#3596) Bumps [mpdf/mpdf](https://github.com/mpdf/mpdf) from 8.1.5 to 8.1.6. - [Release notes](https://github.com/mpdf/mpdf/releases) - [Changelog](https://github.com/mpdf/mpdf/blob/development/CHANGELOG.md) - [Commits](https://github.com/mpdf/mpdf/compare/v8.1.5...v8.1.6) --- updated-dependencies: - dependency-name: mpdf/mpdf dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 112 +++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 61 deletions(-) diff --git a/composer.lock b/composer.lock index b80350619d..2756d8f477 100644 --- a/composer.lock +++ b/composer.lock @@ -1450,27 +1450,27 @@ }, { "name": "mpdf/mpdf", - "version": "v8.1.5", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "c264ce27af0d794ecd04e201b7e37a06b8a9d720" + "reference": "146c7c1dfd21c826b9d5bbfe3c15e52fd933c90f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/c264ce27af0d794ecd04e201b7e37a06b8a9d720", - "reference": "c264ce27af0d794ecd04e201b7e37a06b8a9d720", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/146c7c1dfd21c826b9d5bbfe3c15e52fd933c90f", + "reference": "146c7c1dfd21c826b9d5bbfe3c15e52fd933c90f", "shasum": "" }, "require": { "ext-gd": "*", "ext-mbstring": "*", + "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", "myclabs/deep-copy": "^1.7", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", - "php-http/message-factory": "^1.0", "psr/http-message": "^1.0", - "psr/log": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", "setasign/fpdi": "^2.1" }, "require-dev": { @@ -1523,7 +1523,51 @@ "type": "custom" } ], - "time": "2023-04-04T15:06:48+00:00" + "time": "2023-05-03T19:36:43+00:00" + }, + { + "name": "mpdf/psr-log-aware-trait", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/7a077416e8f39eb626dee4246e0af99dd9ace275", + "reference": "7a077416e8f39eb626dee4246e0af99dd9ace275", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrLogAwareTrait\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + } + ], + "description": "Trait to allow support of different psr/log versions.", + "support": { + "issues": "https://github.com/mpdf/psr-log-aware-trait/issues", + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v2.0.0" + }, + "time": "2023-05-03T06:18:28+00:00" }, { "name": "myclabs/deep-copy", @@ -1891,60 +1935,6 @@ }, "time": "2022-09-06T12:16:56+00:00" }, - { - "name": "php-http/message-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/1.1.0" - }, - "time": "2023-04-14T14:16:17+00:00" - }, { "name": "phpcompatibility/php-compatibility", "version": "9.3.5", From 1c6e2dbc9af6e6384f95c45232f9e58987026326 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:36:59 -0700 Subject: [PATCH 10/89] Bump phpstan/phpstan-phpunit from 1.3.11 to 1.3.13 (#3597) Bumps [phpstan/phpstan-phpunit](https://github.com/phpstan/phpstan-phpunit) from 1.3.11 to 1.3.13. - [Release notes](https://github.com/phpstan/phpstan-phpunit/releases) - [Commits](https://github.com/phpstan/phpstan-phpunit/compare/1.3.11...1.3.13) --- updated-dependencies: - dependency-name: phpstan/phpstan-phpunit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index 2756d8f477..1a769305a4 100644 --- a/composer.lock +++ b/composer.lock @@ -1999,16 +1999,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.14", + "version": "1.10.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d232901b09e67538e5c86a724be841bea5768a7c" + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d232901b09e67538e5c86a724be841bea5768a7c", - "reference": "d232901b09e67538e5c86a724be841bea5768a7c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", "shasum": "" }, "require": { @@ -2057,20 +2057,20 @@ "type": "tidelift" } ], - "time": "2023-04-19T13:47:27+00:00" + "time": "2023-05-09T15:28:01+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.11", + "version": "1.3.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c" + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c", - "reference": "9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5", "shasum": "" }, "require": { @@ -2107,9 +2107,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.11" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.13" }, - "time": "2023-03-25T19:42:13+00:00" + "time": "2023-05-26T11:05:59+00:00" }, { "name": "phpunit/php-code-coverage", From 9b9e30c58224c4348cd221f888c7ad72d11b2235 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:25:09 -0700 Subject: [PATCH 11/89] Upgrade php-cs-fixer (#3602) Dependabot wanted to do this, but the changes to php-cs-fixer caused it to report errors in 7 modules, all due to spacing in comments. I hate this, but who asked me? Just fix them. --- composer.lock | 12 ++++++------ src/PhpSpreadsheet/Calculation/MathTrig/Sum.php | 2 +- src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php | 2 +- src/PhpSpreadsheet/Style/Conditional.php | 2 +- src/PhpSpreadsheet/Writer/Xls.php | 9 +++++---- src/PhpSpreadsheet/Writer/Xls/Parser.php | 12 ++++-------- src/PhpSpreadsheet/Writer/Xls/Workbook.php | 8 +++----- src/PhpSpreadsheet/Writer/Xlsx/StringTable.php | 2 +- 8 files changed, 22 insertions(+), 27 deletions(-) diff --git a/composer.lock b/composer.lock index 1a769305a4..7ac3157236 100644 --- a/composer.lock +++ b/composer.lock @@ -1239,16 +1239,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.16.0", + "version": "v3.17.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "d40f9436e1c448d309fa995ab9c14c5c7a96f2dc" + "reference": "3f0ed862f22386c55a767461ef5083bddceeed79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/d40f9436e1c448d309fa995ab9c14c5c7a96f2dc", - "reference": "d40f9436e1c448d309fa995ab9c14c5c7a96f2dc", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3f0ed862f22386c55a767461ef5083bddceeed79", + "reference": "3f0ed862f22386c55a767461ef5083bddceeed79", "shasum": "" }, "require": { @@ -1323,7 +1323,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.16.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.17.0" }, "funding": [ { @@ -1331,7 +1331,7 @@ "type": "github" } ], - "time": "2023-04-02T19:30:06+00:00" + "time": "2023-05-22T19:59:32+00:00" }, { "name": "masterminds/html5", diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php index 1a797c8a24..56b0861c6e 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -66,8 +66,8 @@ public static function sumErroringStrings(...$args) $returnValue += (int) $arg; } elseif (ErrorValue::isError($arg)) { return $arg; - // ignore non-numerics from cell, but fail as literals (except null) } elseif ($arg !== null && !Functions::isCellValue($k)) { + // ignore non-numerics from cell, but fail as literals (except null) return ExcelError::VALUE(); } } diff --git a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php index bce5fe34a4..5d5babc3f4 100644 --- a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php +++ b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php @@ -163,7 +163,7 @@ public function stream_seek($offset, $whence) // @codingStandardsIgnoreLine $this->pos = $offset; } elseif ($whence == SEEK_CUR && -$offset <= $this->pos) { $this->pos += $offset; - // @phpstan-ignore-next-line + // @phpstan-ignore-next-line } elseif ($whence == SEEK_END && -$offset <= count(/** @scrutinizer ignore-type */ $this->data)) { $this->pos = strlen($this->data) + $offset; } else { diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index de565d3458..36069b00c8 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -248,7 +248,7 @@ public function getConditions() /** * Set Conditions. * - * @param bool|float|int|string|(bool|float|int|string)[] $conditions Condition + * @param (bool|float|int|string)[]|bool|float|int|string $conditions Condition * * @return $this */ diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index 33a404d42d..983414fccc 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -751,11 +751,12 @@ private function writeDocumentSummaryInformation() $dataSection_Content .= $dataProp['data']['data']; $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']); - // Condition below can never be true - //} elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) - // $dataSection_Content .= $dataProp['data']['data']; + /* Condition below can never be true + } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + $dataSection_Content .= $dataProp['data']['data']; - // $dataSection_Content_Offset += 4 + 8; + $dataSection_Content_Offset += 4 + 8; + */ } else { $dataSection_Content .= $dataProp['data']['data']; diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 6b98395f5a..f195ac782b 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -643,7 +643,6 @@ private function convertRange2d($range, $class = 0) // TODO: use real error codes throw new WriterException('Unknown range separator'); } - // Convert the cell references [$row1, $col1] = $this->cellToPackedRowcol($cell1); [$row2, $col2] = $this->cellToPackedRowcol($cell2); @@ -1109,8 +1108,8 @@ private function match($token) if (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { return $token; } - // If it's a string (of maximum 255 characters) if (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) { + // If it's a string (of maximum 255 characters) return $token; } // If it's an error code @@ -1219,21 +1218,18 @@ private function expression() $this->advance(); return $result; - // If it's an error code - } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') { + } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') { // error code $result = $this->createTree($this->currentToken, 'ptgErr', ''); $this->advance(); return $result; - // If it's a negative value - } elseif ($this->currentToken == '-') { + } elseif ($this->currentToken == '-') { // negative value // catch "-" Term $this->advance(); $result2 = $this->expression(); return $this->createTree('ptgUminus', $result2, ''); - // If it's a positive value - } elseif ($this->currentToken == '+') { + } elseif ($this->currentToken == '+') { // positive value // catch "+" Term $this->advance(); $result2 = $this->expression(); diff --git a/src/PhpSpreadsheet/Writer/Xls/Workbook.php b/src/PhpSpreadsheet/Writer/Xls/Workbook.php index 6e9b265dc5..3c68847aa9 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xls/Workbook.php @@ -643,9 +643,8 @@ private function writeAllDefinedNamesBiff8(): string // store the DEFINEDNAME record $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); - - // (exclusive) either repeatColumns or repeatRows } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) { + // (exclusive) either repeatColumns or repeatRows. // Columns to repeat if ($sheetSetup->isColumnsToRepeatAtLeftSet()) { $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); @@ -1102,16 +1101,15 @@ private function writeSharedStringsTable() // 2. space remaining is greater than or equal to minimum space needed // here we write as much as we can in the current block, then move to next record data block - // 1. space remaining is less than minimum space needed if ($space_remaining < $min_space_needed) { + // 1. space remaining is less than minimum space needed. // we close the block, store the block data $recordDatas[] = $recordData; // and start new record data block where we start writing the string $recordData = ''; - - // 2. space remaining is greater than or equal to minimum space needed } else { + // 2. space remaining is greater than or equal to minimum space needed. // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below $effective_space_remaining = $space_remaining; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index 92af57dda1..29e95eb2fe 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -64,7 +64,7 @@ public function createStringTable(ActualWorksheet $worksheet, $existingTable = n /** * Write string table to XML format. * - * @param (string|RichText)[] $stringTable + * @param (RichText|string)[] $stringTable * * @return string XML Output */ From 3aab263580ccc34376fb1016e795c7c7b06acbd1 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 1 Jun 2023 22:17:30 -0700 Subject: [PATCH 12/89] Upgrade mitoteam/jpgraph (#3603) They have made some changes at my request, the major effect of which is that it will now work with 33_Chart_create_bar_stacked. This is a departure for them in that they have changed the functionality of jpgraph, not merely made sure that it is compatible with new Php releases. --- README.md | 2 +- composer.json | 2 +- composer.lock | 14 +++++++------- samples/Chart/35_Chart_render33.php | 5 +---- .../Chart/Renderer/MtJpGraphRenderer.php | 6 +++--- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 79e6e80def..a69c3afc95 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ or the appropriate PDF Writer wrapper for the library that you have chosen to in For Chart export, we support following packages, which you will also need to install yourself using `composer require` - [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0. You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) - - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) - fork with modern PHP versions support. + - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) - up to date fork with modern PHP versions support and some bugs fixed. and then configure PhpSpreadsheet using: ```php diff --git a/composer.json b/composer.json index 1d6f34792e..4b05be334b 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-main", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.2.4", + "mitoteam/jpgraph": "^10.3", "mpdf/mpdf": "^8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 7ac3157236..d4836fe1a9 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": "8c1ccd9ddeaa0f55e5fe86f2d53cc334", + "content-hash": "202b1e25cc7e8a5216ffa899dc111cda", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1404,16 +1404,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.2.6", + "version": "v10.3.1", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "a36181cf31918b810d809816458083c4fac7d7e6" + "reference": "891f7c7a90c3a1b059f157d8b4765c7033da8a5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/a36181cf31918b810d809816458083c4fac7d7e6", - "reference": "a36181cf31918b810d809816458083c4fac7d7e6", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/891f7c7a90c3a1b059f157d8b4765c7033da8a5d", + "reference": "891f7c7a90c3a1b059f157d8b4765c7033da8a5d", "shasum": "" }, "require": { @@ -1444,9 +1444,9 @@ ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.2.6" + "source": "https://github.com/mitoteam/jpgraph/tree/v10.3.1" }, - "time": "2023-03-10T11:02:47+00:00" + "time": "2023-05-31T14:15:30+00:00" }, { "name": "mpdf/mpdf", diff --git a/samples/Chart/35_Chart_render33.php b/samples/Chart/35_Chart_render33.php index bd8075de47..97091fceec 100644 --- a/samples/Chart/35_Chart_render33.php +++ b/samples/Chart/35_Chart_render33.php @@ -24,10 +24,7 @@ $unresolvedErrors = []; } else { $unresolvedErrors = [ - // The following spreadsheet was created by 3rd party software, - // and doesn't include the data that usually accompanies a chart. - // That is good enough for Excel, but not for JpGraph. - '33_Chart_create_bar_stacked.xlsx', + //'33_Chart_create_bar_stacked.xlsx', // fixed with mitoteam/jpgraph 10.3 ]; } foreach ($inputFileNames as $inputFileName) { diff --git a/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php index e1f0f90add..b5e70d3a19 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php +++ b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php @@ -3,12 +3,12 @@ namespace PhpOffice\PhpSpreadsheet\Chart\Renderer; /** - * Jpgraph is not oficially maintained in Composer. + * Jpgraph is not officially maintained by Composer at packagist.org. * * This renderer implementation uses package * https://packagist.org/packages/mitoteam/jpgraph * - * This package is up to date for August 2022 and has PHP 8.1 support. + * This package is up to date for June 2023 and has PHP 8.2 support. */ class MtJpGraphRenderer extends JpGraphRendererBase { @@ -29,7 +29,7 @@ protected static function init(): void 'regstat', 'scatter', 'stock', - ]); + ], true); // enable Extended mode $loaded = true; } From a0a9b2be214a3dca468110e5fc53a3ca68963a50 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 2 Jun 2023 23:25:38 -0700 Subject: [PATCH 13/89] HyperlinkBase Property, and Html Handling of Properties (#3589) * HyperlinkBase Property, and Html Handling of Properties Fix #3573. The original issue concerned non-support of Document Properties in Xml spreadsheets. However, most of the Properties mentioned there were already supported. But the investigation revealed some gaps in Html coverage. HyperlinkBase is the one property mentioned in the issue that was not supported for Xml, nor indeed for any other format. All the other document properties are 'meta'; but HyperlinkBase is functional - if you supply a relative address for a link, Excel will use HyperlinkBase, if supplied, to convert to an absolute address. (Default is directory where spreadsheet is located.) Here's a summary of how this PR will handle this property for various formats: - Support is added for Xlsx read and write. - Support is added for Xml read (there is no Xml writer). Ironically, Excel messes up this processing when reading an Xml spreadsheet; however, PhpSpreadsheet will get it right. - HyperlinkBase is supported for Xls, but I have no idea how to read or write this property. For now, when writing hyperlinked cells, PhpSpreadsheet will be changed to convert any relative addresses that it can detect to absolute references by adding HyperlinkBase to the relative address. In a similar vein, Xls supports custom properties, but PhpSpreadsheet does not know how to read or write those. - Gnumeric has no equivalent property, so nothing needs to be done to its reader. Since we don't have a Gnumeric writer, that's not really a problem for us. - Odt has no equivalent property, so nothing needs to be done to its reader. The Odt writer does not have any special logic for hyperlinks, so, at least for now, will remain unchanged. - Csv has no equivalent property, so nothing needs to be done to its reader. The Csv writer does not have any special logic for hyperlinks, so, at least for now, will remain unchanged. - Html allows for an equivalent `base` tag in the head section. Support for this is added to Html reader and writer. Html Writer was only handling 8 of the 11 'core' properties. Support is added for `created`, `modified`, and `lastModifiedBy`. Custom properties were not supported at all, and now are. Html Reader did not support any properties. It will now support all of them. * Scrutinizer Remove one dead reference. --- src/PhpSpreadsheet/Document/Properties.php | 14 ++ src/PhpSpreadsheet/Reader/Html.php | 89 ++++++++- src/PhpSpreadsheet/Reader/Xlsx/Properties.php | 3 + src/PhpSpreadsheet/Reader/Xml/Properties.php | 19 +- src/PhpSpreadsheet/Writer/Html.php | 42 +++- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 7 + src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 3 + .../Reader/Xml/XmlPropertiesTest.php | 187 ++++++++++++++++++ tests/data/Reader/Xml/hyperlinkbase.xml | 91 +++++++++ 9 files changed, 445 insertions(+), 10 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php create mode 100644 tests/data/Reader/Xml/hyperlinkbase.xml diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 162e818036..302afee79e 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -107,6 +107,8 @@ class Properties */ private $customProperties = []; + private string $hyperlinkBase = ''; + /** * Create a new Document Properties instance. */ @@ -534,4 +536,16 @@ public static function convertPropertyType(string $propertyType): string { return self::PROPERTY_TYPE_ARRAY[$propertyType] ?? self::PROPERTY_TYPE_UNKNOWN; } + + public function getHyperlinkBase(): string + { + return $this->hyperlinkBase; + } + + public function setHyperlinkBase(string $hyperlinkBase): self + { + $this->hyperlinkBase = $hyperlinkBase; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 377a8add06..fa3db908ef 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -8,6 +8,7 @@ use DOMText; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -685,10 +686,94 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) if ($loaded === false) { throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null); } + self::loadProperties($dom, $spreadsheet); return $this->loadDocument($dom, $spreadsheet); } + private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void + { + $properties = $spreadsheet->getProperties(); + foreach ($dom->getElementsByTagName('meta') as $meta) { + $metaContent = (string) $meta->getAttribute('content'); + if ($metaContent !== '') { + $metaName = (string) $meta->getAttribute('name'); + switch ($metaName) { + case 'author': + $properties->setCreator($metaContent); + + break; + case 'category': + $properties->setCategory($metaContent); + + break; + case 'company': + $properties->setCompany($metaContent); + + break; + case 'created': + $properties->setCreated($metaContent); + + break; + case 'description': + $properties->setDescription($metaContent); + + break; + case 'keywords': + $properties->setKeywords($metaContent); + + break; + case 'lastModifiedBy': + $properties->setLastModifiedBy($metaContent); + + break; + case 'manager': + $properties->setManager($metaContent); + + break; + case 'modified': + $properties->setModified($metaContent); + + break; + case 'subject': + $properties->setSubject($metaContent); + + break; + case 'title': + $properties->setTitle($metaContent); + + break; + default: + if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) { + switch ($matches[1]) { + case 'bool': + $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN); + + break; + case 'float': + $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT); + + break; + case 'int': + $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER); + + break; + case 'date': + $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE); + + break; + default: // string + $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING); + } + } + } + } + } + if (!empty($dom->baseURI)) { + $properties->setHyperlinkBase($dom->baseURI); + } + } + private static function replaceNonAscii(array $matches): string { return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; @@ -719,8 +804,10 @@ public function loadFromString($content, ?Spreadsheet $spreadsheet = null): Spre if ($loaded === false) { throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null); } + $spreadsheet = $spreadsheet ?? new Spreadsheet(); + self::loadProperties($dom, $spreadsheet); - return $this->loadDocument($dom, $spreadsheet ?? new Spreadsheet()); + return $this->loadDocument($dom, $spreadsheet); } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php index 72addffd5b..0d4701afac 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -73,6 +73,9 @@ public function readExtendedProperties(string $propertyData): void if (isset($xmlCore->Manager)) { $this->docProps->setManager((string) $xmlCore->Manager); } + if (isset($xmlCore->HyperlinkBase)) { + $this->docProps->setHyperlinkBase((string) $xmlCore->HyperlinkBase); + } } } diff --git a/src/PhpSpreadsheet/Reader/Xml/Properties.php b/src/PhpSpreadsheet/Reader/Xml/Properties.php index f0346ed02f..e216c254da 100644 --- a/src/PhpSpreadsheet/Reader/Xml/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xml/Properties.php @@ -92,6 +92,10 @@ protected function processStandardProperty( case 'Manager': $docProps->setManager($stringValue); + break; + case 'HyperlinkBase': + $docProps->setHyperlinkBase($stringValue); + break; case 'Keywords': $docProps->setKeywords($stringValue); @@ -110,17 +114,10 @@ protected function processCustomProperty( ?SimpleXMLElement $propertyValue, SimpleXMLElement $propertyAttributes ): void { - $propertyType = DocumentProperties::PROPERTY_TYPE_UNKNOWN; - switch ((string) $propertyAttributes) { - case 'string': - $propertyType = DocumentProperties::PROPERTY_TYPE_STRING; - $propertyValue = trim((string) $propertyValue); - - break; case 'boolean': $propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN; - $propertyValue = (bool) $propertyValue; + $propertyValue = (bool) (string) $propertyValue; break; case 'integer': @@ -134,9 +131,15 @@ protected function processCustomProperty( break; case 'dateTime.tz': + case 'dateTime.iso8601tz': $propertyType = DocumentProperties::PROPERTY_TYPE_DATE; $propertyValue = trim((string) $propertyValue); + break; + default: + $propertyType = DocumentProperties::PROPERTY_TYPE_STRING; + $propertyValue = trim((string) $propertyValue); + break; } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 575197aecb..842998f9eb 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -7,9 +7,11 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; use PhpOffice\PhpSpreadsheet\Settings; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont; @@ -342,13 +344,21 @@ public function writeAllSheets() private static function generateMeta(?string $val, string $desc): string { - return $val + return ($val || $val === '0') ? (' ' . PHP_EOL) : ''; } public const BODY_LINE = ' ' . PHP_EOL; + private const CUSTOM_TO_META = [ + Properties::PROPERTY_TYPE_BOOLEAN => 'bool', + Properties::PROPERTY_TYPE_DATE => 'date', + Properties::PROPERTY_TYPE_FLOAT => 'float', + Properties::PROPERTY_TYPE_INTEGER => 'int', + Properties::PROPERTY_TYPE_STRING => 'string', + ]; + /** * Generate HTML header. * @@ -374,6 +384,36 @@ public function generateHTMLHeader($includeStyles = false) $html .= self::generateMeta($properties->getCategory(), 'category'); $html .= self::generateMeta($properties->getCompany(), 'company'); $html .= self::generateMeta($properties->getManager(), 'manager'); + $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy'); + $date = Date::dateTimeFromTimestamp((string) $properties->getCreated()); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $html .= self::generateMeta($date->format(DATE_W3C), 'created'); + $date = Date::dateTimeFromTimestamp((string) $properties->getModified()); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $html .= self::generateMeta($date->format(DATE_W3C), 'modified'); + + $customProperties = $properties->getCustomProperties(); + foreach ($customProperties as $customProperty) { + $propertyValue = $properties->getCustomPropertyValue($customProperty); + $propertyType = $properties->getCustomPropertyType($customProperty); + $propertyQualifier = self::CUSTOM_TO_META[$propertyType] ?? null; + if ($propertyQualifier !== null) { + if ($propertyType === Properties::PROPERTY_TYPE_BOOLEAN) { + $propertyValue = $propertyValue ? '1' : '0'; + } elseif ($propertyType === Properties::PROPERTY_TYPE_DATE) { + $date = Date::dateTimeFromTimestamp((string) $propertyValue); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $propertyValue = $date->format(DATE_W3C); + } else { + $propertyValue = (string) $propertyValue; + } + $html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty"); + } + } + + if (!empty($properties->getHyperlinkBase())) { + $html .= ' ' . PHP_EOL; + } $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 9f23bd365e..aeedd08e77 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -503,6 +503,8 @@ public function close(): void $this->writeMergedCells(); // Hyperlinks + $phpParent = $phpSheet->getParent(); + $hyperlinkbase = ($phpParent === null) ? '' : $phpParent->getProperties()->getHyperlinkBase(); foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) { [$column, $row] = Coordinate::indexesFromString($coordinate); @@ -513,6 +515,11 @@ public function close(): void $url = str_replace('sheet://', 'internal:', $url); } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) { // URL + } elseif (!empty($hyperlinkbase) && preg_match('~^([A-Za-z]:)?[/\\\\]~', $url) !== 1) { + $url = "$hyperlinkbase$url"; + if (preg_match('/^(http:|https:|ftp:|mailto:)/', $url) !== 1) { + $url = 'external:' . $url; + } } else { // external (local file) $url = 'external:' . $url; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 8902826a19..8c33f59326 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -93,6 +93,9 @@ public function writeDocPropsApp(Spreadsheet $spreadsheet) // SharedDoc $objWriter->writeElement('SharedDoc', 'false'); + // HyperlinkBase + $objWriter->writeElement('HyperlinkBase', $spreadsheet->getProperties()->getHyperlinkBase()); + // HyperlinksChanged $objWriter->writeElement('HyperlinksChanged', 'false'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php new file mode 100644 index 0000000000..8b4a225d3f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlPropertiesTest.php @@ -0,0 +1,187 @@ +load($this->filename); + + $properties = $spreadsheet->getProperties(); + self::assertSame('title', $properties->getTitle()); + self::assertSame('topic', $properties->getSubject()); + self::assertSame('author', $properties->getCreator()); + self::assertSame('keyword1, keyword2', $properties->getKeywords()); + self::assertSame('no comment', $properties->getDescription()); + self::assertSame('last author', $properties->getLastModifiedBy()); + $expected = self::timestampToInt('2023-05-18T11:21:43Z'); + self::assertEquals($expected, $properties->getCreated()); + $expected = self::timestampToInt('2023-05-18T11:30:00Z'); + self::assertEquals($expected, $properties->getModified()); + self::assertSame('category', $properties->getCategory()); + self::assertSame('manager', $properties->getManager()); + self::assertSame('company', $properties->getCompany()); + + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/', $properties->getHyperlinkBase()); + + self::assertSame('TheString', $properties->getCustomPropertyValue('StringProperty')); + self::assertSame(12345, $properties->getCustomPropertyValue('NumberProperty')); + $expected = self::timestampToInt('2023-05-18T10:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty')); + $expected = self::timestampToInt('2023-05-19T11:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty2')); + self::assertTrue($properties->getCustomPropertyValue('BooleanPropertyTrue')); + self::assertFalse($properties->getCustomPropertyValue('BooleanPropertyFalse')); + self::assertEqualsWithDelta(1.2345, $properties->getCustomPropertyValue('FloatProperty'), 1E-8); + + $sheet = $spreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // It will, however, work just fine in the Xlsx and Html copies. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('references/features-cross-reference/', $hyperlink->getUrl()); + // Same comment as for cell above. + self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $properties = $reloadedSpreadsheet->getProperties(); + self::assertSame('title', $properties->getTitle()); + self::assertSame('topic', $properties->getSubject()); + self::assertSame('author', $properties->getCreator()); + self::assertSame('keyword1, keyword2', $properties->getKeywords()); + self::assertSame('no comment', $properties->getDescription()); + self::assertSame('last author', $properties->getLastModifiedBy()); + $expected = self::timestampToInt('2023-05-18T11:21:43Z'); + self::assertEquals($expected, $properties->getCreated()); + $expected = self::timestampToInt('2023-05-18T11:30:00Z'); + self::assertEquals($expected, $properties->getModified()); + self::assertSame('category', $properties->getCategory()); + self::assertSame('manager', $properties->getManager()); + self::assertSame('company', $properties->getCompany()); + + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/', $properties->getHyperlinkBase()); + + self::assertSame('TheString', $properties->getCustomPropertyValue('StringProperty')); + self::assertSame(12345, $properties->getCustomPropertyValue('NumberProperty')); + // Note that Xlsx will ignore the time part when displaying + // the property. + $expected = self::timestampToInt('2023-05-18T10:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty')); + $expected = self::timestampToInt('2023-05-19T11:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty2')); + self::assertTrue($properties->getCustomPropertyValue('BooleanPropertyTrue')); + self::assertFalse($properties->getCustomPropertyValue('BooleanPropertyFalse')); + self::assertEqualsWithDelta(1.2345, $properties->getCustomPropertyValue('FloatProperty'), 1E-8); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // It will, however, work just fine in the Xlsx and Html copies. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('references/features-cross-reference/', $hyperlink->getUrl()); + // Same comment as for cell above. + self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testPropertiesHtml(): void + { + $reader = new Xml(); + $spreadsheet = $reader->load($this->filename); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Html'); + $spreadsheet->disconnectWorksheets(); + + $properties = $reloadedSpreadsheet->getProperties(); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/', $properties->getHyperlinkBase()); + + self::assertSame('title', $properties->getTitle()); + self::assertSame('topic', $properties->getSubject()); + self::assertSame('author', $properties->getCreator()); + self::assertSame('keyword1, keyword2', $properties->getKeywords()); + self::assertSame('no comment', $properties->getDescription()); + self::assertSame('last author', $properties->getLastModifiedBy()); + $expected = self::timestampToInt('2023-05-18T11:21:43Z'); + self::assertEquals($expected, $properties->getCreated()); + $expected = self::timestampToInt('2023-05-18T11:30:00Z'); + self::assertEquals($expected, $properties->getModified()); + self::assertSame('category', $properties->getCategory()); + self::assertSame('manager', $properties->getManager()); + self::assertSame('company', $properties->getCompany()); + + self::assertSame('TheString', $properties->getCustomPropertyValue('StringProperty')); + self::assertSame(12345, $properties->getCustomPropertyValue('NumberProperty')); + $expected = self::timestampToInt('2023-05-18T10:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty')); + $expected = self::timestampToInt('2023-05-19T11:00:00Z'); + self::assertEquals($expected, $properties->getCustomPropertyValue('DateProperty2')); + self::assertTrue($properties->getCustomPropertyValue('BooleanPropertyTrue')); + self::assertFalse($properties->getCustomPropertyValue('BooleanPropertyFalse')); + self::assertEqualsWithDelta(1.2345, $properties->getCustomPropertyValue('FloatProperty'), 1E-8); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // It will, however, work just fine in the Xlsx and Html copies. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('references/features-cross-reference/', $hyperlink->getUrl()); + // Same comment as for cell above. + self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testHyperlinksXls(): void + { + $reader = new Xml(); + $spreadsheet = $reader->load($this->filename); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + // Note that relative links don't actually work in XML format. + // However, Xls Writer will convert relative to absolute. + $hyperlink = $sheet->getCell('A1')->getHyperlink(); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/references/features-cross-reference/', $hyperlink->getUrl()); + // Xls writer does not get involved in function call. + // However, hyperlink does get updated somewhere. + //self::assertSame('topics/accessing-cells/', $sheet->getCell('A2')->getCalculatedValue()); + $hyperlink = $sheet->getCell('A2')->getHyperlink(); + self::assertSame('https://phpspreadsheet.readthedocs.io/en/latest/topics/accessing-cells/', $hyperlink->getUrl()); + // No problem for absolute links. + $hyperlink = $sheet->getCell('A3')->getHyperlink(); + self::assertSame('https://www.google.com/', $hyperlink->getUrl()); + self::assertSame('https://www.yahoo.com', $sheet->getCell('A4')->getCalculatedValue()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + private static function timestampToInt(string $timestamp): string + { + $dto = new DateTimeImmutable($timestamp); + + return $dto->format('U'); + } +} diff --git a/tests/data/Reader/Xml/hyperlinkbase.xml b/tests/data/Reader/Xml/hyperlinkbase.xml new file mode 100644 index 0000000000..e3448ee994 --- /dev/null +++ b/tests/data/Reader/Xml/hyperlinkbase.xml @@ -0,0 +1,91 @@ + + + + + title + topic + author + keyword1, keyword2 + no comment + last author + 2023-05-18T11:21:43Z + 2023-05-18T11:30:00Z + category + manager + company + https://phpspreadsheet.readthedocs.io/en/latest/ + 16.00 + + + TheString + 12345 + 2023-05-18T10:00:00Z + 2023-05-19T11:00:00Z + 1 + 0 + 1.2345 + + + + + + 6820 + 19200 + 32767 + 32767 + False + False + + + + + + + + + references/features-cross-reference/ + + + topics/accessing-cells/ + + + https://www.google.com + + + https://www.yahoo.com + +
+ + + + + + 3 + 3 + + + False + False + +
+
From a2edbf877608bcd5aecebff930ef37864425250c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 3 Jun 2023 07:53:54 -0700 Subject: [PATCH 14/89] Unzipped Gnumeric File (#3591) * Unzipped Gnumeric File A Gnumeric file is normally a gzipped Xml file. However, the Gnumeric application will also read an unzipped Xml file. Allow PhpSpreadsheet to do the same. * Scrutinizer (legitimate) Fix minor problem. --- src/PhpSpreadsheet/Reader/Gnumeric.php | 51 ++- tests/PhpSpreadsheetTests/IOFactoryTest.php | 1 + .../Reader/Gnumeric/GnumericInfoTest.php | 27 +- .../Reader/Gnumeric/GnumericLoadTest.php | 35 +- .../Reader/Gnumeric/PageSetupTest.php | 20 +- .../Reader/Xml/XmlInfoTest.php | 41 ++- .../Gnumeric/PageSetup.gnumeric.unzipped.xml | 348 ++++++++++++++++++ 7 files changed, 479 insertions(+), 44 deletions(-) create mode 100644 tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 1745532257..99e4d6ad6c 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -80,17 +80,15 @@ public function __construct() */ public function canRead(string $filename): bool { - // Check if gzlib functions are available - if (File::testFileNoThrow($filename) && function_exists('gzread')) { - // Read signature data (first 3 bytes) - $fh = fopen($filename, 'rb'); - if ($fh !== false) { - $data = fread($fh, 2); - fclose($fh); + $data = null; + if (File::testFileNoThrow($filename)) { + $data = $this->gzfileGetContents($filename); + if (strpos($data, self::NAMESPACE_GNM) === false) { + $data = ''; } } - return isset($data) && $data === chr(0x1F) . chr(0x8B); + return !empty($data); } private static function matchXml(XMLReader $xml, string $expectedLocalName): bool @@ -110,9 +108,13 @@ private static function matchXml(XMLReader $xml, string $expectedLocalName): boo public function listWorksheetNames($filename) { File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an invalid Gnumeric file.'); + } $xml = new XMLReader(); - $xml->xml($this->getSecurityScannerOrThrow()->scanFile('compress.zlib://' . realpath($filename)), null, Settings::getLibXmlLoaderOptions()); + $contents = $this->gzfileGetContents($filename); + $xml->xml($contents, null, Settings::getLibXmlLoaderOptions()); $xml->setParserProperty(2, true); $worksheetNames = []; @@ -139,9 +141,13 @@ public function listWorksheetNames($filename) public function listWorksheetInfo($filename) { File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an invalid Gnumeric file.'); + } $xml = new XMLReader(); - $xml->xml($this->getSecurityScannerOrThrow()->scanFile('compress.zlib://' . realpath($filename)), null, Settings::getLibXmlLoaderOptions()); + $contents = $this->gzfileGetContents($filename); + $xml->xml($contents, null, Settings::getLibXmlLoaderOptions()); $xml->setParserProperty(2, true); $worksheetInfo = []; @@ -185,13 +191,23 @@ public function listWorksheetInfo($filename) */ private function gzfileGetContents($filename) { - $file = @gzopen($filename, 'rb'); $data = ''; - if ($file !== false) { - while (!gzeof($file)) { - $data .= gzread($file, 1024); + $contents = @file_get_contents($filename); + if ($contents !== false) { + if (substr($contents, 0, 2) === "\x1f\x8b") { + // Check if gzlib functions are available + if (function_exists('gzdecode')) { + $contents = @gzdecode($contents); + if ($contents !== false) { + $data = $contents; + } + } + } else { + $data = $contents; } - gzclose($file); + } + if ($data !== '') { + $data = $this->getSecurityScannerOrThrow()->scan($data); } return $data; @@ -245,10 +261,13 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp { $this->spreadsheet = $spreadsheet; File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an invalid Gnumeric file.'); + } $gFileData = $this->gzfileGetContents($filename); - $xml2 = simplexml_load_string($this->getSecurityScannerOrThrow()->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); + $xml2 = simplexml_load_string($gFileData, 'SimpleXMLElement', Settings::getLibXmlLoaderOptions()); $xml = self::testSimpleXml($xml2); $gnmXML = $xml->children(self::NAMESPACE_GNM); diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index 5c0e657ecf..4f46a6b268 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -97,6 +97,7 @@ public static function providerIdentify(): array return [ ['samples/templates/26template.xlsx', 'Xlsx', Reader\Xlsx::class], ['samples/templates/GnumericTest.gnumeric', 'Gnumeric', Reader\Gnumeric::class], + ['tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml', 'Gnumeric', Reader\Gnumeric::class], ['samples/templates/old.gnumeric', 'Gnumeric', Reader\Gnumeric::class], ['samples/templates/30template.xls', 'Xls', Reader\Xls::class], ['samples/templates/OOCalcTest.ods', 'Ods', Reader\Ods::class], diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericInfoTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericInfoTest.php index 9ec16128dd..7bc0077639 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericInfoTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericInfoTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Gnumeric; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PHPUnit\Framework\TestCase; @@ -9,9 +10,7 @@ class GnumericInfoTest extends TestCase { public function testListNames(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/GnumericTest.gnumeric'; + $filename = 'samples/templates/GnumericTest.gnumeric'; $reader = new Gnumeric(); $names = $reader->listWorksheetNames($filename); self::assertCount(2, $names); @@ -21,9 +20,7 @@ public function testListNames(): void public function testListInfo(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/GnumericTest.gnumeric'; + $filename = 'samples/templates/GnumericTest.gnumeric'; $reader = new Gnumeric(); $info = $reader->listWorksheetInfo($filename); $expected = [ @@ -44,4 +41,22 @@ public function testListInfo(): void ]; self::assertEquals($expected, $info); } + + public function testListNamesNotGumeric(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $filename = 'samples/templates/excel2003.xml'; + $reader = new Gnumeric(); + $reader->listWorksheetNames($filename); + } + + public function testListInfoNotXml(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $filename = __FILE__; + $reader = new Gnumeric(); + $reader->listWorksheetInfo($filename); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php index 0cf2b2289d..c413b3a745 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/GnumericLoadTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Gnumeric; use DateTimeZone; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Style\Border; @@ -16,9 +17,7 @@ class GnumericLoadTest extends TestCase { public function testLoad(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/GnumericTest.gnumeric'; + $filename = 'samples/templates/GnumericTest.gnumeric'; $reader = new Gnumeric(); $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); @@ -132,9 +131,7 @@ public function testLoad(): void public function testLoadFilter(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/GnumericTest.gnumeric'; + $filename = 'samples/templates/GnumericTest.gnumeric'; $reader = new Gnumeric(); $filter = new GnumericFilter(); $reader->setReadFilter($filter); @@ -150,9 +147,7 @@ public function testLoadFilter(): void public function testLoadOld(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/old.gnumeric'; + $filename = 'samples/templates/old.gnumeric'; $reader = new Gnumeric(); $spreadsheet = $reader->load($filename); $props = $spreadsheet->getProperties(); @@ -162,9 +157,7 @@ public function testLoadOld(): void public function testLoadSelectedSheets(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/GnumericTest.gnumeric'; + $filename = 'samples/templates/GnumericTest.gnumeric'; $reader = new Gnumeric(); $reader->setLoadSheetsOnly(['Unknown Sheet', 'Report Data']); $spreadsheet = $reader->load($filename); @@ -176,4 +169,22 @@ public function testLoadSelectedSheets(): void self::assertSame('A1', $sheet->getSelectedCells()); $spreadsheet->disconnectWorksheets(); } + + public function testLoadNotGnumeric(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $filename = 'samples/templates/excel2003.xml'; + $reader = new Gnumeric(); + $reader->load($filename); + } + + public function testLoadNotXml(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('invalid Gnumeric file'); + $filename = __FILE__; + $reader = new Gnumeric(); + $reader->load($filename); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php index a851c44c75..bac8f42506 100644 --- a/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/PageSetupTest.php @@ -10,9 +10,19 @@ class PageSetupTest extends TestCase { private const MARGIN_PRECISION = 0.001; - public function testPageSetup(): void + public static function fileProvider(): array + { + return [ + ['tests/data/Reader/Gnumeric/PageSetup.gnumeric'], + ['tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml'], + ]; + } + + /** + * @dataProvider fileProvider + */ + public function testPageSetup(string $filename): void { - $filename = 'tests/data/Reader/Gnumeric/PageSetup.gnumeric'; $reader = new Gnumeric(); $spreadsheet = $reader->load($filename); $assertions = $this->pageSetupAssertions(); @@ -39,9 +49,11 @@ public function testPageSetup(): void $spreadsheet->disconnectWorksheets(); } - public function testPageMargins(): void + /** + * @dataProvider fileProvider + */ + public function testPageMargins(string $filename): void { - $filename = 'tests/data/Reader/Gnumeric/PageSetup.gnumeric'; $reader = new Gnumeric(); $spreadsheet = $reader->load($filename); $assertions = $this->pageMarginAssertions(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlInfoTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlInfoTest.php index 1abbbcf9d4..9bab187792 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlInfoTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlInfoTest.php @@ -10,9 +10,7 @@ class XmlInfoTest extends TestCase { public function testListNames(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/excel2003.xml'; + $filename = 'samples/templates/excel2003.xml'; $reader = new Xml(); $names = $reader->listWorksheetNames($filename); self::assertCount(2, $names); @@ -23,17 +21,26 @@ public function testListNames(): void public function testListNamesInvalidFile(): void { $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid Spreadsheet file'); $filename = __FILE__; $reader = new Xml(); $names = $reader->listWorksheetNames($filename); self::assertNotEquals($names, $names); } + public function testListNamesGnumericFile(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid Spreadsheet file'); + $filename = 'tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml'; + $reader = new Xml(); + $names = $reader->listWorksheetNames($filename); + self::assertNotEquals($names, $names); + } + public function testListInfo(): void { - $filename = __DIR__ - . '/../../../..' - . '/samples/templates/excel2003.xml'; + $filename = 'samples/templates/excel2003.xml'; $reader = new Xml(); $info = $reader->listWorksheetInfo($filename); $expected = [ @@ -58,18 +65,40 @@ public function testListInfo(): void public function testListInfoInvalidFile(): void { $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid Spreadsheet file'); $filename = __FILE__; $reader = new Xml(); $info = $reader->listWorksheetInfo($filename); self::assertNotEquals($info, $info); } + public function testListInfoGnumericFile(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid Spreadsheet file'); + $filename = 'tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml'; + $reader = new Xml(); + $info = $reader->listWorksheetInfo($filename); + self::assertNotEquals($info, $info); + } + public function testLoadInvalidFile(): void { $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid Spreadsheet file'); $filename = __FILE__; $reader = new Xml(); $spreadsheet = $reader->load($filename); self::assertNotEquals($spreadsheet, $spreadsheet); } + + public function testLoadGnumericFile(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid Spreadsheet file'); + $filename = 'tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml'; + $reader = new Xml(); + $spreadsheet = $reader->load($filename); + self::assertNotEquals($spreadsheet, $spreadsheet); + } } diff --git a/tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml b/tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml new file mode 100644 index 0000000000..fb98dd5adf --- /dev/null +++ b/tests/data/Reader/Gnumeric/PageSetup.gnumeric.unzipped.xml @@ -0,0 +1,348 @@ + + + + + + WorkbookView::show_horizontal_scrollbar + TRUE + + + WorkbookView::show_vertical_scrollbar + TRUE + + + WorkbookView::show_notebook_tabs + TRUE + + + WorkbookView::do_auto_completion + TRUE + + + WorkbookView::is_protected + FALSE + + + + + Mark Baker + 2020-07-04T15:11:06Z + + false + false + 0 + 2020-06-29T17:37:00Z + Mark Baker + 2020-07-04T11:51:41Z + false + false + + + + + Sheet1 + Sheet2 + Sheet3 + Sheet4 + + + + + Sheet1 + 2 + 4 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet1" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + iso_a4 + + + + + + + + + + + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + =sum(A2:C2,B1:B3) + =count(A2:C2,B1:B3) + + + + + + Sheet2 + 2 + 4 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet2" + A1 + + + + + + + + + + + + + + + + + + + + + + r_then_d + landscape + + + iso_a4 + + + + + + + + + + + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + =sum($A$1:$C$1,$A$3:$C$3,$A$1:$A$3,$C$1:$C$3) + =count($A$1:$C$1,$A$3:$C$3,$A$1:$A$3,$C$1:$C$3) + + + + + + Sheet3 + 2 + 4 + 1 + + + Print_Area + Sheet3!$A$1:$C$5 + A1 + + + Sheet_Title + "Sheet3" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + iso_a4 + + + + + + + + + + + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + + Sheet4 + 2 + 4 + 1 + + + Print_Area + #REF! + A1 + + + Sheet_Title + "Sheet4" + A1 + + + + + + + + + + + + + + + + + + + + + + d_then_r + portrait + + + iso_a4 + + + + + + + + + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + From e258918a812e3f06493268b80a390d1276ca7a1b Mon Sep 17 00:00:00 2001 From: jpachta Date: Thu, 8 Jun 2023 07:09:12 +0200 Subject: [PATCH 15/89] Update reading-files.md (#3607) Fixed typo in code examples in name of variable `$columnAddress` that was wrongly shortened to `$column`. Long version of the variable name $columnAddress is used in the corresponding samples scripts so now the docs match the samples. --- docs/topics/reading-files.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md index 767052812c..27b6bb60c6 100644 --- a/docs/topics/reading-files.md +++ b/docs/topics/reading-files.md @@ -303,7 +303,7 @@ class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter public function readCell($columnAddress, $row, $worksheetName = '') { // Read rows 1 to 7 and columns A to E only if ($row >= 1 && $row <= 7) { - if (in_array($column,range('A','E'))) { + if (in_array($columnAddress,range('A','E'))) { return true; } } @@ -348,7 +348,7 @@ class MyReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter public function readCell($columnAddress, $row, $worksheetName = '') { // Only read the rows and columns that were configured if ($row >= $this->startRow && $row <= $this->endRow) { - if (in_array($column,$this->columns)) { + if (in_array($columnAddress,$this->columns)) { return true; } } From a1d960ba8181e00ee705838028f2145c9e50a94f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:34:00 -0700 Subject: [PATCH 16/89] Improvements for Data Validation (#3605) Fix #3592. Fix #3594. The original issue asked that Data Validation be added for Xml spreadsheets. However, doing so exposed some other Data Validation problems, which are corrected along with the Xml portion. The main other problem is that the code for `Cell\DataValidator::isValid` covered almost no possibilities. It is expanded to include all possibilities, except for Custom Types, which I will continue to think about. Many tests are added. An obscure problem is that Xlsx Reader does not quite work properly when the Data Validation cells are beyond the high-used column or row. We use the high-used values because it is completely impractical to set all the cells in a range when an entire column or row is selected. This change causes the leftmost top cell in a range to be explicitly allocated, affecting the high-used values. It is not 100% effective, but it will be much more difficult to get into the problem situation, which I will continue to think about. Although our expectation is that we will not see any Xml Spreadsheets using non-standard namespacing, much of the Xml Reader code is changed to use proper namespace techniques. A few areas which just had nothing to do with this change continue to use less robust techniques; I plan to address those soon after implementing this change. One of the Data Validation types is for time of day. Testing revealed that `Shared\Date::convertIsoDate` was insufficiently robust when determining whether its input consisted of a time of day without date. It is now more robust. --- src/PhpSpreadsheet/Cell/DataValidator.php | 54 ++++- .../Reader/Xlsx/DataValidations.php | 12 + src/PhpSpreadsheet/Reader/Xml.php | 126 ++++++----- .../Reader/Xml/DataValidations.php | 177 +++++++++++++++ .../Reader/Xml/PageSettings.php | 9 +- src/PhpSpreadsheet/Shared/Date.php | 2 +- src/PhpSpreadsheet/Worksheet/Validations.php | 5 +- .../Reader/Xml/DataValidationsTest.php | 136 +++++++++++ .../Reader/Xml/XmlFreezePanesTest.php | 43 ++++ tests/data/Reader/Xml/datavalidations.xml | 212 ++++++++++++++++++ 10 files changed, 705 insertions(+), 71 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Xml/DataValidations.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/DataValidationsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/XmlFreezePanesTest.php create mode 100644 tests/data/Reader/Xml/datavalidations.xml diff --git a/src/PhpSpreadsheet/Cell/DataValidator.php b/src/PhpSpreadsheet/Cell/DataValidator.php index 0e395a7ff9..692f316ec0 100644 --- a/src/PhpSpreadsheet/Cell/DataValidator.php +++ b/src/PhpSpreadsheet/Cell/DataValidator.php @@ -20,7 +20,7 @@ class DataValidator */ public function isValid(Cell $cell) { - if (!$cell->hasDataValidation()) { + if (!$cell->hasDataValidation() || $cell->getDataValidation()->getType() === DataValidation::TYPE_NONE) { return true; } @@ -31,13 +31,55 @@ public function isValid(Cell $cell) return false; } - // TODO: write check on all cases - switch ($dataValidation->getType()) { - case DataValidation::TYPE_LIST: - return $this->isValueInList($cell); + $returnValue = false; + $type = $dataValidation->getType(); + if ($type === DataValidation::TYPE_LIST) { + $returnValue = $this->isValueInList($cell); + } elseif ($type === DataValidation::TYPE_WHOLE) { + if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) { + $returnValue = false; + } else { + $returnValue = $this->numericOperator($dataValidation, (int) $cellValue); + } + } elseif ($type === DataValidation::TYPE_DECIMAL || $type === DataValidation::TYPE_DATE || $type === DataValidation::TYPE_TIME) { + if (!is_numeric($cellValue)) { + $returnValue = false; + } else { + $returnValue = $this->numericOperator($dataValidation, (float) $cellValue); + } + } elseif ($type === DataValidation::TYPE_TEXTLENGTH) { + $returnValue = $this->numericOperator($dataValidation, mb_strlen((string) $cellValue)); + } + + return $returnValue; + } + + /** @param float|int $cellValue */ + private function numericOperator(DataValidation $dataValidation, $cellValue): bool + { + $operator = $dataValidation->getOperator(); + $formula1 = $dataValidation->getFormula1(); + $formula2 = $dataValidation->getFormula2(); + $returnValue = false; + if ($operator === DataValidation::OPERATOR_BETWEEN) { + $returnValue = $cellValue >= $formula1 && $cellValue <= $formula2; + } elseif ($operator === DataValidation::OPERATOR_NOTBETWEEN) { + $returnValue = $cellValue < $formula1 || $cellValue > $formula2; + } elseif ($operator === DataValidation::OPERATOR_EQUAL) { + $returnValue = $cellValue == $formula1; + } elseif ($operator === DataValidation::OPERATOR_NOTEQUAL) { + $returnValue = $cellValue != $formula1; + } elseif ($operator === DataValidation::OPERATOR_LESSTHAN) { + $returnValue = $cellValue < $formula1; + } elseif ($operator === DataValidation::OPERATOR_LESSTHANOREQUAL) { + $returnValue = $cellValue <= $formula1; + } elseif ($operator === DataValidation::OPERATOR_GREATERTHAN) { + $returnValue = $cellValue > $formula1; + } elseif ($operator === DataValidation::OPERATOR_GREATERTHANOREQUAL) { + $returnValue = $cellValue >= $formula1; } - return false; + return $returnValue; } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index dac76230cc..210c322f9d 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -22,6 +22,18 @@ public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml public function load(): void { + foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { + // Uppercase coordinate + $range = strtoupper((string) $dataValidation['sqref']); + $rangeSet = explode(' ', $range); + foreach ($rangeSet as $range) { + if (preg_match('/^[A-Z]{1,3}\\d{1,7}/', $range, $matches) === 1) { + // Ensure left/top row of range exists, thereby + // adjusting high row/column. + $this->worksheet->getCell($matches[0]); + } + } + } foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { // Uppercase coordinate $range = strtoupper((string) $dataValidation['sqref']); diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 7b8697aff2..6be26fc2c6 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces; use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings; use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xml\Style; @@ -26,6 +27,8 @@ */ class Xml extends BaseReader { + public const NAMESPACES_SS = 'urn:schemas-microsoft-com:office:spreadsheet'; + /** * Formats. * @@ -146,11 +149,9 @@ public function listWorksheetNames($filename) throw new Exception("Problem reading {$filename}"); } - $namespaces = $xml->getNamespaces(true); - - $xml_ss = $xml->children($namespaces['ss']); + $xml_ss = $xml->children(self::NAMESPACES_SS); foreach ($xml_ss->Worksheet as $worksheet) { - $worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); + $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS); $worksheetNames[] = (string) $worksheet_ss['Name']; } @@ -178,12 +179,10 @@ public function listWorksheetInfo($filename) throw new Exception("Problem reading {$filename}"); } - $namespaces = $xml->getNamespaces(true); - $worksheetID = 1; - $xml_ss = $xml->children($namespaces['ss']); + $xml_ss = $xml->children(self::NAMESPACES_SS); foreach ($xml_ss->Worksheet as $worksheet) { - $worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); + $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS); $tmpInfo = []; $tmpInfo['worksheetName'] = ''; @@ -288,12 +287,12 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo } $worksheetID = 0; - $xml_ss = $xml->children($namespaces['ss']); + $xml_ss = $xml->children(self::NAMESPACES_SS); /** @var null|SimpleXMLElement $worksheetx */ foreach ($xml_ss->Worksheet as $worksheetx) { $worksheet = $worksheetx ?? new SimpleXMLElement(''); - $worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); + $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS); if ( isset($this->loadSheetsOnly, $worksheet_ss['Name']) && @@ -321,7 +320,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo // locally scoped defined names if (isset($worksheet->Names[0])) { foreach ($worksheet->Names[0] as $definedName) { - $definedName_ss = self::getAttributes($definedName, $namespaces['ss']); + $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS); $name = (string) $definedName_ss['Name']; $definedValue = (string) $definedName_ss['RefersTo']; $convertedValue = AddressHelper::convertFormulaToA1($definedValue); @@ -335,7 +334,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $columnID = 'A'; if (isset($worksheet->Table->Column)) { foreach ($worksheet->Table->Column as $columnData) { - $columnData_ss = self::getAttributes($columnData, $namespaces['ss']); + $columnData_ss = self::getAttributes($columnData, self::NAMESPACES_SS); $colspan = 0; if (isset($columnData_ss['Span'])) { $spanAttr = (string) $columnData_ss['Span']; @@ -372,7 +371,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $additionalMergedCells = 0; foreach ($worksheet->Table->Row as $rowData) { $rowHasData = false; - $row_ss = self::getAttributes($rowData, $namespaces['ss']); + $row_ss = self::getAttributes($rowData, self::NAMESPACES_SS); if (isset($row_ss['Index'])) { $rowID = (int) $row_ss['Index']; } @@ -383,7 +382,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $columnID = 'A'; foreach ($rowData->Cell as $cell) { - $cell_ss = self::getAttributes($cell, $namespaces['ss']); + $cell_ss = self::getAttributes($cell, self::NAMESPACES_SS); if (isset($cell_ss['Index'])) { $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']); } @@ -425,7 +424,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $cellData = $cell->Data; $cellValue = (string) $cellData; $type = DataType::TYPE_NULL; - $cellData_ss = self::getAttributes($cellData, $namespaces['ss']); + $cellData_ss = self::getAttributes($cellData, self::NAMESPACES_SS); if (isset($cellData_ss['Type'])) { $cellDataType = $cellData_ss['Type']; switch ($cellDataType) { @@ -483,7 +482,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo } if (isset($cell->Comment)) { - $this->parseCellComment($cell->Comment, $namespaces, $spreadsheet, $columnID, $rowID); + $this->parseCellComment($cell->Comment, $spreadsheet, $columnID, $rowID); } if (isset($cell_ss['StyleID'])) { @@ -512,47 +511,57 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo ++$rowID; } + } - if (isset($namespaces['x'])) { - $xmlX = $worksheet->children($namespaces['x']); - if (isset($xmlX->WorksheetOptions)) { - (new PageSettings($xmlX, $namespaces))->loadPageSettings($spreadsheet); - if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) { - $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible; - $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible; - if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) { - $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1); - $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate); - } - } - $rangeCalculated = false; - if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) { - if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) { - $selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) - . $selectionMatches[1] - . ':' - . Coordinate::stringFromColumnIndex((int) $selectionMatches[4]) - . $selectionMatches[3]; - $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell); - $rangeCalculated = true; - } - } - if (!$rangeCalculated) { - if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) { - $activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow; - } else { - $activeRow = 0; - } - if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) { - $activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol; - } else { - $activeColumn = 0; - } - if (is_numeric($activeRow) && is_numeric($activeColumn)) { - $selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1); - $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell); - } - } + $dataValidations = new Xml\DataValidations(); + $dataValidations->loadDataValidations($worksheet, $spreadsheet); + $xmlX = $worksheet->children(Namespaces::URN_EXCEL); + if (isset($xmlX->WorksheetOptions)) { + if (isset($xmlX->WorksheetOptions->FreezePanes)) { + $freezeRow = $freezeColumn = 1; + if (isset($xmlX->WorksheetOptions->SplitHorizontal)) { + $freezeRow = (int) $xmlX->WorksheetOptions->SplitHorizontal + 1; + } + if (isset($xmlX->WorksheetOptions->SplitVertical)) { + $freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1; + } + $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow); + } + (new PageSettings($xmlX))->loadPageSettings($spreadsheet); + if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) { + $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible; + $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible; + if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) { + $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1); + $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate); + } + } + $rangeCalculated = false; + if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) { + if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) { + $selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) + . $selectionMatches[1] + . ':' + . Coordinate::stringFromColumnIndex((int) $selectionMatches[4]) + . $selectionMatches[3]; + $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell); + $rangeCalculated = true; + } + } + if (!$rangeCalculated) { + if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) { + $activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow; + } else { + $activeRow = 0; + } + if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) { + $activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol; + } else { + $activeColumn = 0; + } + if (is_numeric($activeRow) && is_numeric($activeColumn)) { + $selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1); + $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell); } } } @@ -567,7 +576,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex); if (isset($xml->Names[0])) { foreach ($xml->Names[0] as $definedName) { - $definedName_ss = self::getAttributes($definedName, $namespaces['ss']); + $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS); $name = (string) $definedName_ss['Name']; $definedValue = (string) $definedName_ss['RefersTo']; $convertedValue = AddressHelper::convertFormulaToA1($definedValue); @@ -584,12 +593,11 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo protected function parseCellComment( SimpleXMLElement $comment, - array $namespaces, Spreadsheet $spreadsheet, string $columnID, int $rowID ): void { - $commentAttributes = $comment->attributes($namespaces['ss']); + $commentAttributes = $comment->attributes(self::NAMESPACES_SS); $author = 'unknown'; if (isset($commentAttributes->Author)) { $author = (string) $commentAttributes->Author; diff --git a/src/PhpSpreadsheet/Reader/Xml/DataValidations.php b/src/PhpSpreadsheet/Reader/Xml/DataValidations.php new file mode 100644 index 0000000000..31748cb9c2 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xml/DataValidations.php @@ -0,0 +1,177 @@ + DataValidation::OPERATOR_BETWEEN, + 'equal' => DataValidation::OPERATOR_EQUAL, + 'greater' => DataValidation::OPERATOR_GREATERTHAN, + 'greaterorequal' => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 'less' => DataValidation::OPERATOR_LESSTHAN, + 'lessorequal' => DataValidation::OPERATOR_LESSTHANOREQUAL, + 'notbetween' => DataValidation::OPERATOR_NOTBETWEEN, + 'notequal' => DataValidation::OPERATOR_NOTEQUAL, + ]; + + private const TYPE_MAPPINGS = [ + 'textlength' => DataValidation::TYPE_TEXTLENGTH, + ]; + + private int $thisRow = 0; + + private int $thisColumn = 0; + + private function replaceR1C1(array $matches): string + { + return AddressHelper::convertToA1($matches[0], $this->thisRow, $this->thisColumn, false); + } + + public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $spreadsheet): void + { + $xmlX = $worksheet->children(Namespaces::URN_EXCEL); + $sheet = $spreadsheet->getActiveSheet(); + /** @var callable */ + $pregCallback = [$this, 'replaceR1C1']; + foreach ($xmlX->DataValidation as $dataValidation) { + $cells = []; + $validation = new DataValidation(); + + // set defaults + $validation->setShowDropDown(true); + $validation->setShowInputMessage(true); + $validation->setShowErrorMessage(true); + $validation->setShowDropDown(true); + $this->thisRow = 1; + $this->thisColumn = 1; + + foreach ($dataValidation as $tagName => $tagValue) { + $tagValue = (string) $tagValue; + $tagValueLower = strtolower($tagValue); + switch ($tagName) { + case 'Range': + foreach (explode(',', $tagValue) as $range) { + $cell = ''; + if (preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) { + // range + $firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) + . $selectionMatches[1]; + $cell = $firstCell + . ':' + . Coordinate::stringFromColumnIndex((int) $selectionMatches[4]) + . $selectionMatches[3]; + $this->thisRow = (int) $selectionMatches[1]; + $this->thisColumn = (int) $selectionMatches[2]; + $sheet->getCell($firstCell); + } elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) { + // cell + $cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) + . $selectionMatches[1]; + $sheet->getCell($cell); + $this->thisRow = (int) $selectionMatches[1]; + $this->thisColumn = (int) $selectionMatches[2]; + } elseif (preg_match('/^C(\d+)$/', (string) $range, $selectionMatches) === 1) { + // column + $firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[1]) + . '1'; + $cell = $firstCell + . ':' + . Coordinate::stringFromColumnIndex((int) $selectionMatches[1]) + . ((string) AddressRange::MAX_ROW); + $this->thisColumn = (int) $selectionMatches[1]; + $sheet->getCell($firstCell); + } elseif (preg_match('/^R(\d+)$/', (string) $range, $selectionMatches)) { + // row + $firstCell = 'A' + . $selectionMatches[1]; + $cell = $firstCell + . ':' + . AddressRange::MAX_COLUMN + . $selectionMatches[1]; + $this->thisRow = (int) $selectionMatches[1]; + $sheet->getCell($firstCell); + } + + $validation->setSqref($cell); + $stRange = $sheet->shrinkRangeToFit($cell); + $cells = array_merge($cells, Coordinate::extractAllCellReferencesInRange($stRange)); + } + + break; + case 'Type': + $validation->setType(self::TYPE_MAPPINGS[$tagValueLower] ?? $tagValueLower); + + break; + case 'Qualifier': + $validation->setOperator(self::OPERATOR_MAPPINGS[$tagValueLower] ?? $tagValueLower); + + break; + case 'InputTitle': + $validation->setPromptTitle($tagValue); + + break; + case 'InputMessage': + $validation->setPrompt($tagValue); + + break; + case 'InputHide': + $validation->setShowInputMessage(false); + + break; + case 'ErrorStyle': + $validation->setErrorStyle($tagValueLower); + + break; + case 'ErrorTitle': + $validation->setErrorTitle($tagValue); + + break; + case 'ErrorMessage': + $validation->setError($tagValue); + + break; + case 'ErrorHide': + $validation->setShowErrorMessage(false); + + break; + case 'ComboHide': + $validation->setShowDropDown(false); + + break; + case 'UseBlank': + $validation->setAllowBlank(true); + + break; + case 'CellRangeList': + // FIXME missing FIXME + + break; + case 'Min': + case 'Value': + $tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue); + $validation->setFormula1($tagValue); + + break; + case 'Max': + $tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue); + $validation->setFormula2($tagValue); + + break; + } + } + + foreach ($cells as $cell) { + $sheet->getCell($cell)->setDataValidation(clone $validation); + } + } + } +} diff --git a/src/PhpSpreadsheet/Reader/Xml/PageSettings.php b/src/PhpSpreadsheet/Reader/Xml/PageSettings.php index 39535c3e71..137cabaf30 100644 --- a/src/PhpSpreadsheet/Reader/Xml/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Xml/PageSettings.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xml; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use SimpleXMLElement; @@ -14,9 +15,9 @@ class PageSettings */ private $printSettings; - public function __construct(SimpleXMLElement $xmlX, array $namespaces) + public function __construct(SimpleXMLElement $xmlX) { - $printSettings = $this->pageSetup($xmlX, $namespaces, $this->getPrintDefaults()); + $printSettings = $this->pageSetup($xmlX, $this->getPrintDefaults()); $this->printSettings = $this->printSetup($xmlX, $printSettings); } @@ -56,13 +57,13 @@ private function getPrintDefaults(): stdClass ]; } - private function pageSetup(SimpleXMLElement $xmlX, array $namespaces, stdClass $printDefaults): stdClass + private function pageSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass { if (isset($xmlX->WorksheetOptions->PageSetup)) { foreach ($xmlX->WorksheetOptions->PageSetup as $pageSetupData) { foreach ($pageSetupData as $pageSetupKey => $pageSetupValue) { /** @scrutinizer ignore-call */ - $pageSetupAttributes = $pageSetupValue->attributes($namespaces['x']); + $pageSetupAttributes = $pageSetupValue->attributes(Namespaces::URN_EXCEL); if ($pageSetupAttributes !== null) { switch ($pageSetupKey) { case 'Layout': diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 26dde93f3a..4f19673113 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -184,7 +184,7 @@ public static function convertIsoDate($value) throw new Exception("Invalid string $value supplied for datatype Date"); } - if (preg_match('/^\\d\\d:\\d\\d:\\d\\d/', $value) == 1) { + if (preg_match('/^\\s*\\d?\\d:\\d\\d(:\\d\\d([.]\\d+)?)?\\s*(am|pm)?\\s*$/i', $value) == 1) { $newValue = fmod($newValue, 1.0); } diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index aab3aae447..42ba566c6c 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -53,6 +53,9 @@ public static function validateCellOrCellRange($cellRange): string return self::validateCellRange($cellRange); } + private const SETMAXROW = '${1}1:${2}' . AddressRange::MAX_ROW; + private const SETMAXCOL = 'A${1}:' . AddressRange::MAX_COLUMN . '${2}'; + /** * Validate a cell range. * @@ -69,7 +72,7 @@ public static function validateCellRange($cellRange): string // or Row ranges like '1:3' to 'A1:XFD3' $addressRange = (string) preg_replace( ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], - ['${1}1:${2}1048576', 'A${1}:XFD${2}'], + [self::SETMAXROW, self::SETMAXCOL], $addressRange ); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/DataValidationsTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/DataValidationsTest.php new file mode 100644 index 0000000000..a90fc63e1c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/DataValidationsTest.php @@ -0,0 +1,136 @@ +load($this->filename); + $sheet = $spreadsheet->getActiveSheet(); + $assertions = $this->validationAssertions(); + $validation = $sheet->getCell('A1')->getDataValidation(); + self::assertSame('A1:A1048576', $validation->getSqref()); + $validation = $sheet->getCell('B3')->getDataValidation(); + self::assertSame('B2:B1048576', $validation->getSqref()); + + foreach ($assertions as $title => $assertion) { + $sheet->getCell($assertion[1])->setValue($assertion[2]); + self::assertSame($assertion[0], $sheet->getCell($assertion[1])->hasValidValue(), $title); + } + $sheet->getCell('F1')->getDataValidation()->setType(DataValidation::TYPE_NONE); + $sheet->getCell('F1')->setValue(1); + self::assertTrue($sheet->getCell('F1')->hasValidValue(), 'validation type is NONE'); + $spreadsheet->disconnectWorksheets(); + } + + public function testValidationXlsx(): void + { + $reader = new Xml(); + $oldspreadsheet = $reader->load($this->filename); + $spreadsheet = $this->writeAndReload($oldspreadsheet, 'Xlsx'); + $oldspreadsheet->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + $assertions = $this->validationAssertions(); + $validation = $sheet->getCell('A1')->getDataValidation(); + self::assertSame('A1:A1048576', $validation->getSqref()); + $validation = $sheet->getCell('B3')->getDataValidation(); + self::assertSame('B2:B1048576', $validation->getSqref()); + + foreach ($assertions as $title => $assertion) { + $sheet->getCell($assertion[1])->setValue($assertion[2]); + self::assertSame($assertion[0], $sheet->getCell($assertion[1])->hasValidValue(), $title); + } + $spreadsheet->disconnectWorksheets(); + } + + public function testValidationXls(): void + { + $reader = new Xml(); + $oldspreadsheet = $reader->load($this->filename); + $spreadsheet = $this->writeAndReload($oldspreadsheet, 'Xls'); + $oldspreadsheet->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + $assertions = $this->validationAssertions(); + //$validation = $sheet->getCell('A1')->getDataValidation(); + //self::assertSame('A1:A1048576', $validation->getSqref()); + //$validation = $sheet->getCell('B3')->getDataValidation(); + //self::assertSame('B2:B1048576', $validation->getSqref()); + + foreach ($assertions as $title => $assertion) { + $sheet->getCell($assertion[1])->setValue($assertion[2]); + self::assertSame($assertion[0], $sheet->getCell($assertion[1])->hasValidValue(), $title); + } + $spreadsheet->disconnectWorksheets(); + } + + private function validationAssertions(): array + { + return [ + // Numeric tests + 'Integer between 2 and 5: x' => [false, 'F1', 'x'], + 'Integer between 2 and 5: 3.1' => [false, 'F1', 3.1], + 'Integer between 2 and 5: 3' => [true, 'F1', 3], + 'Integer between 2 and 5: 1' => [false, 'F1', 1], + 'Integer between 2 and 5: 7' => [false, 'F1', 7], + 'Float between 2 and 5: x' => [false, 'G1', 'x'], + 'Float between 2 and 5: 3.1' => [true, 'G1', 3.1], + 'Float between 2 and 5: 3' => [true, 'G1', 3], + 'Float between 2 and 5: 1' => [false, 'G1', 1], + 'Float between 2 and 5: 7' => [false, 'G1', 7], + 'Integer not between -5 and 5: 3' => [false, 'F2', 3], + 'Integer not between -5 and 5: -1' => [false, 'F2', -1], + 'Integer not not between -5 and 5: 7' => [true, 'F2', 7], + 'Any integer except 7: -1' => [true, 'F3', -1], + 'Any integer except 7: 7' => [false, 'F3', 7], + 'Only -3: -1' => [false, 'F4', -1], + 'Only -3: -3' => [true, 'F4', -3], + 'Integer less than 8: 8' => [false, 'F5', 8], + 'Integer less than 8: 7' => [true, 'F5', 7], + 'Integer less than 8: 9' => [false, 'F5', 9], + 'Integer less than or equal 12: 12' => [true, 'F6', 12], + 'Integer less than or equal 12: 7' => [true, 'F6', 7], + 'Integer less than or equal 12: 13' => [false, 'F6', 13], + 'Integer greater than or equal -6: -6' => [true, 'F7', -6], + 'Integer greater than or equal -6: -7' => [false, 'F7', -7], + 'Integer greater than or equal -6: -5' => [true, 'F7', -5], + 'Integer greater than 5: 5' => [false, 'F8', 5], + 'Integer greater than 5: 6' => [true, 'F8', 6], + 'Integer greater than 5: 3' => [false, 'F8', 3], + // Text tests + 'a,b,c,d,e: a' => [true, 'C4', 'a'], + 'a,b,c,d,e: c' => [true, 'C4', 'c'], + 'a,b,c,d,e: e' => [true, 'C4', 'e'], + 'a,b,c,d,e: x' => [false, 'C4', 'x'], + 'a,b,c,d,e: aa' => [false, 'C4', 'aa'], + 'less than 8 characters: abcdefg' => [true, 'C3', 'abcdefg'], + 'less than 8 characters: abcdefgh' => [false, 'C3', 'abcdefgh'], + 'texts in e1 to e5: ccc' => [true, 'D2', 'ccc'], + 'texts in e1 to e5: ffffff' => [false, 'D2', 'ffffff'], + 'date from 20230101: 20221231' => [false, 'C1', Date::convertIsoDate('20221231')], + 'date from 20230101: 20230101' => [true, 'C1', Date::convertIsoDate('20230101')], + 'date from 20230101: 20240507' => [true, 'C1', Date::convertIsoDate('20240507')], + 'date from 20230101: 20240507 10:00:00' => [true, 'C1', Date::convertIsoDate('20240507 10:00:00')], + 'time from 12:00-14:00: 2023-01-01 13:00:00' => [false, 'C2', Date::convertIsoDate('2023-01-01 13:00:00')], + 'time from 8:00-14:00: 13:00' => [true, 'C2', Date::convertIsoDate('13:00')], + 'time from 8:00-14:00: 07:00:00' => [false, 'C2', Date::convertIsoDate('07:00:00')], + 'time from 8:00-14:00: 15:00:00' => [false, 'C2', Date::convertIsoDate('15:00:00')], + 'time from 8:00-14:00: 1:13 am' => [false, 'C2', Date::convertIsoDate('1:13 am')], + 'time from 8:00-14:00: 1:13 pm' => [true, 'C2', Date::convertIsoDate('1:13 pm')], + 'time from 8:00-14:00: 9:13' => [true, 'C2', Date::convertIsoDate('9:13')], + 'time from 8:00-14:00: 9:13 am' => [true, 'C2', Date::convertIsoDate('9:13 am')], + 'time from 8:00-14:00: 9:13 pm' => [false, 'C2', Date::convertIsoDate('9:13 pm')], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlFreezePanesTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlFreezePanesTest.php new file mode 100644 index 0000000000..440bbf50f0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlFreezePanesTest.php @@ -0,0 +1,43 @@ + + + + + +
+ + + 2 + 10 + 6 + 20 + +
+
+ EOT; + $reader = new Xml(); + $spreadsheet = $reader->loadSpreadsheetFromString($xmldata); + self::assertEquals(1, $spreadsheet->getSheetCount()); + + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Tabelle1', $sheet->getTitle()); + self::assertSame('G3', $sheet->getFreezePane()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/Xml/datavalidations.xml b/tests/data/Reader/Xml/datavalidations.xml new file mode 100644 index 0000000000..5fe5a874a0 --- /dev/null +++ b/tests/data/Reader/Xml/datavalidations.xml @@ -0,0 +1,212 @@ + + + + + Owen Leibman + 2023-05-31T15:52:55Z + 2023-06-02T02:15:24Z + 16.00 + + + true + 2023-06-02T02:15:24Z + Standard + defa4170-0d19-0005-0004-bc88714345d2 + f9465cb1-7889-4d9a-b552-fdd0addf0eb1 + a9441577-fd50-4686-b29c-06250a20f19e + 0 + + + + + + 6780 + 19160 + 32767 + 32767 + False + False + + + + + + + + + decimal numbers below: + a + + + bb + + + ccc + + + dddd + + + eeeee + + + ffffff + +
+ + + + + + 3 + 1 + 2 + + + False + False + + + C1 + Whole + 1 + 9 + Enter a digit + Please enter a single digit from 1 to 9 + Sorry, only digits from 1 to 9 are allowed + Wrong number + + + R2C2:R1048576C2 + Decimal + Greater + 10 + Decimal number bigger than 10 + + + R1C2 + Greater + + Decimal number bigger than 10 + + + R1C3 + Date + GreaterOrEqual + 44927 + date from 2023-01-01 + + + R2C3 + Time + 0.333333333333333 + 0.583333333333333 + time from 8:00 to 14:00 + + + R3C3 + TextLength + Less + 8 + text < 8 characters + + + R4C3 + List + + + "a,b,c,d,e" + letters a to e + + + R1C4 + Whole + 1+1 + 2+2 + integer from 2 to 4 + + + R2C4 + List + R[-1]C[1]:R[3]C[1] + any of the texts in E1:E5 + + + R7C6 + Whole + GreaterOrEqual + -6 + Integer greater than or equal to -6 + + + R6C6 + Whole + LessOrEqual + 12 + Integer less than or equal to 12 + + + R5C6 + Whole + Less + 8 + Integer less than 8 + + + R4C6 + Whole + Equal + -3 + Negative 3 is only valid input + + + R3C6 + Whole + NotEqual + 7 + Any integer except 7 + + + R2C6 + Whole + NotBetween + -5 + 5 + Integer not between -5 and 5 + + + R1C6 + Whole + 2 + 5 + Integer between 2 and 5 + + + R8C6 + Whole + Greater + 5 + Integer greater than 5 + + + R1C7 + Decimal + 2 + 5 + Float between 2 and 5 + +
+
From eba727113493a1a2334b1e46eb3b9339d1993649 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:30:56 -0700 Subject: [PATCH 17/89] Changelog Updates 20230609 (#3612) Added a few changes not documented since last update. --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35658bb637..3e2ac65e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Font/Effects/Theme support for Chart Data Labels and Axis. [PR #3476](https://github.com/PHPOffice/PhpSpreadsheet/pull/3476) - Font Themes support. [PR #3486](https://github.com/PHPOffice/PhpSpreadsheet/pull/3486) - Ability to Ignore Cell Errors in Excel. [Issue #1141](https://github.com/PHPOffice/PhpSpreadsheet/issues/1141) [PR #3508](https://github.com/PHPOffice/PhpSpreadsheet/pull/3508) +- Unzipped Gnumeric file [PR #3591](https://github.com/PHPOffice/PhpSpreadsheet/pull/3591) ### Changed @@ -60,7 +61,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Allow Index_number as Array for VLOOKUP/HLOOKUP [Issue #3561](https://github.com/PHPOffice/PhpSpreadsheet/issues/3561 [PR #3570](https://github.com/PHPOffice/PhpSpreadsheet/pull/3570) - Add Unsupported Options in Xml Spreadsheet [Issue #3566](https://github.com/PHPOffice/PhpSpreadsheet/issues/3566 [Issue #3568](https://github.com/PHPOffice/PhpSpreadsheet/issues/3568 [Issue #3569](https://github.com/PHPOffice/PhpSpreadsheet/issues/3569 [PR #3567](https://github.com/PHPOffice/PhpSpreadsheet/pull/3567) - Changes to NUMBERVALUE, VALUE, DATEVALUE, TIMEVALUE [Issue #3574](https://github.com/PHPOffice/PhpSpreadsheet/issues/3574 [PR #3575](https://github.com/PHPOffice/PhpSpreadsheet/pull/3575) - +- Redo calculation of color tinting [Issue #3550](https://github.com/PHPOffice/PhpSpreadsheet/issues/3550) [PR #3580](https://github.com/PHPOffice/PhpSpreadsheet/pull/3580) +- Accommodate Slash with preg_quote [PR #3582](https://github.com/PHPOffice/PhpSpreadsheet/pull/3582) [PR #3583](https://github.com/PHPOffice/PhpSpreadsheet/pull/3583) [PR #3584](https://github.com/PHPOffice/PhpSpreadsheet/pull/3584) +- HyperlinkBase Property and Html Handling of Properties [Issue #3573](https://github.com/PHPOffice/PhpSpreadsheet/issues/3573) [PR #3589](https://github.com/PHPOffice/PhpSpreadsheet/pull/3589) +- Improvements for Data Validation [Issue #3592](https://github.com/PHPOffice/PhpSpreadsheet/issues/3592) [Issue #3594](https://github.com/PHPOffice/PhpSpreadsheet/issues/3594) [PR #3605](https://github.com/PHPOffice/PhpSpreadsheet/pull/3605) ## 1.28.0 - 2023-02-25 From 570660f12ad740c251802575428485d5db34531a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:06:05 -0700 Subject: [PATCH 18/89] Column Widths, Especially for ODS (#3610) Fix #3609. Reporter created a spreadsheet with non-adjacent columns having non-default widths. Ods Writer needs to generate entries for the missing columns with default width. The fix was fairly simple. Testing it was not. Ods Reader basically ignores all styles; they are complicated, declared in (at least) 2 places (content.xml and styles.xml). This change deals only with the problem as reported, in which the missing declarations should be in content.xml. If someone reports a real-world example of this involving styles.xml, I can look at that then. In the meantime, this toehold might serve as a template for adding other style processing to Ods Reader. Looking at other formats, processing of column widths was also missing from Html Reader, and is now added. Note that this will work only with inline Css declarations on the `col` tags, which can be generated by Html Writer using `setUseInlineCss(true)`. This creates a much larger file than one created without inline CSS. A general problem became evident when studying the code. Worksheet `columnDimensions` is an unsorted array. This is not a problem per se, but it can easily lead to unexpected results from a `getColumnDimensions` call. The code is changed to sort the array before returning it in `getColumnDimensions`. One existing test failed as a result of this change. It errorneously tested `getHighestColumn` instead of `getHighestDataColumn`, which caused a problem because the final column declaration included a `number-columns-repeated` attribute. The new result for `getHighestColumn` is correct, and the test is changed to use `getHighestDataColumn` instead, which was certainly its intent. --- src/PhpSpreadsheet/Reader/Html.php | 28 +++++--- src/PhpSpreadsheet/Reader/Ods.php | 40 ++++++++++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 9 +++ src/PhpSpreadsheet/Writer/Ods/Content.php | 9 +++ .../Reader/Ods/OdsTest.php | 4 +- .../Worksheet/ColumnDimension2Test.php | 65 +++++++++++++++++++ 6 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index fa3db908ef..bfb52401aa 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -491,9 +491,12 @@ private function processDomElementImg(Worksheet $sheet, int &$row, string &$colu } } + private string $currentColumn = 'A'; + private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { if ($child->nodeName === 'table') { + $this->currentColumn = 'A'; $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); $column = $this->setTableStartColumn($column); if ($this->tableLevel > 1 && $row > 1) { @@ -513,7 +516,10 @@ private function processDomElementTable(Worksheet $sheet, int &$row, string &$co private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void { - if ($child->nodeName === 'tr') { + if ($child->nodeName === 'col') { + $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray); + ++$this->currentColumn; + } elseif ($child->nodeName === 'tr') { $column = $this->getTableStartColumn(); $cellContent = ''; $this->processDomElement($child, $sheet, $row, $column, $cellContent); @@ -877,7 +883,9 @@ private function applyInlineStyle(Worksheet &$sheet, $row, $column, $attributeAr return; } - if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + if ($row <= 0 || $column === '') { + $cellStyle = new Style(); + } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { $columnTo = $column; for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { ++$columnTo; @@ -1009,16 +1017,20 @@ private function applyInlineStyle(Worksheet &$sheet, $row, $column, $attributeAr break; case 'width': - $sheet->getColumnDimension($column)->setWidth( - (new CssDimension($styleValue ?? ''))->width() - ); + if ($column !== '') { + $sheet->getColumnDimension($column)->setWidth( + (new CssDimension($styleValue ?? ''))->width() + ); + } break; case 'height': - $sheet->getRowDimension($row)->setRowHeight( - (new CssDimension($styleValue ?? ''))->height() - ); + if ($row > 0) { + $sheet->getRowDimension($row)->setRowHeight( + (new CssDimension($styleValue ?? ''))->height() + ); + } break; diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 2508154007..9913f33252 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -8,6 +8,7 @@ use DOMNode; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension; use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator; @@ -295,11 +296,29 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) $tableNs = $dom->lookupNamespaceUri('table'); $textNs = $dom->lookupNamespaceUri('text'); $xlinkNs = $dom->lookupNamespaceUri('xlink'); + $styleNs = $dom->lookupNamespaceUri('style'); $pageSettings->readStyleCrossReferences($dom); $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); $definedNameReader = new DefinedNames($spreadsheet, $tableNs); + $columnWidths = []; + $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0); + $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style'); + foreach ($automaticStyles as $automaticStyle) { + $styleName = $automaticStyle->getAttributeNS($styleNs, 'name'); + $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family'); + if ($styleFamily === 'table-column') { + $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties'); + if ($tcprops !== null) { + $tcprop = $tcprops->item(0); + if ($tcprop !== null) { + $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width'); + $columnWidths[$styleName] = $columnWidth; + } + } + } + } // Content $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0); @@ -340,6 +359,7 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) // Go through every child of table element $rowID = 1; + $tableColumnIndex = 1; foreach ($worksheetDataSet->childNodes as $childNode) { /** @var DOMElement $childNode */ @@ -366,6 +386,26 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet) // $rowData = $cellData; // break; // } + break; + case 'table-column': + if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $rowRepeats = 1; + } + $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); + if (isset($columnWidths[$tableStyleName])) { + $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); + $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex); + for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) { + $spreadsheet->getActiveSheet() + ->getColumnDimension($tableColumnString) + ->setWidth($columnWidth->toUnit('cm'), 'cm'); + ++$tableColumnString; + } + } + $tableColumnIndex += $rowRepeats; + break; case 'table-row': if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 06f86cd173..29221e9910 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -543,9 +543,18 @@ public function getDefaultRowDimension() */ public function getColumnDimensions() { + /** @var callable */ + $callable = [self::class, 'columnDimensionCompare']; + uasort($this->columnDimensions, $callable); + return $this->columnDimensions; } + private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int + { + return $a->getColumnNumeric() - $b->getColumnNumeric(); + } + /** * Get default column dimension. * diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index e931421afd..e0a729ab83 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -126,7 +126,16 @@ private function writeSheets(XMLWriter $objWriter): void $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1)); $objWriter->writeElement('office:forms'); + $lastColumn = 0; foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { + $thisColumn = $columnDimension->getColumnNumeric(); + $emptyColumns = $thisColumn - $lastColumn - 1; + if ($emptyColumns > 0) { + $objWriter->startElement('table:table-column'); + $objWriter->writeAttribute('table:number-columns-repeated', (string) $emptyColumns); + $objWriter->endElement(); + } + $lastColumn = $thisColumn; $objWriter->startElement('table:table-column'); $objWriter->writeAttribute( 'table:style-name', diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 47fc13d8ec..f7c769cd1b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -141,8 +141,8 @@ public function testReadValueAndComments(): void $firstSheet = $spreadsheet->getSheet(0); - self::assertEquals(29, $firstSheet->getHighestRow()); - self::assertEquals('N', $firstSheet->getHighestColumn()); + self::assertEquals(29, $firstSheet->getHighestDataRow()); + self::assertEquals('N', $firstSheet->getHighestDataColumn()); // Simple cell value self::assertEquals('Test String 1', $firstSheet->getCell('A1')->getValue()); diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php new file mode 100644 index 0000000000..f5878eef56 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnDimension2Test.php @@ -0,0 +1,65 @@ +getActiveSheet(); + $expectedCm = 0.45; + foreach ($columns as $column) { + $sheet->getColumnDimension($column)->setWidth($expectedCm, 'cm'); + } + if ($type === 'Html') { + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $type, null, [self::class, 'inlineCss']); + } else { + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $type); + } + $spreadsheet->disconnectWorksheets(); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + for ($column = 'A'; $column !== 'Z'; ++$column) { + if (in_array($column, $columns, true)) { + self::assertEqualsWithDelta($expectedCm, $sheet->getColumnDimension($column)->getWidth(Dimension::UOM_CENTIMETERS), 1E-3, "column $column"); + } elseif ($type === 'Xls' && $column <= 'T') { + // Xls is a bit weird. Columns through max used + // actually set a width in the spreadsheet file. + // Columns above max used obviously do not. + self::assertEqualsWithDelta(9.140625, $sheet->getColumnDimension($column)->getWidth(), 1E-3, "column $column"); + } elseif ($type === 'Html' && $column <= 'T') { + // Html is a lot like Xls. Columns through max used + // actually set a width in the spreadsheet file. + // Columns above max used obviously do not. + self::assertEqualsWithDelta(7.998, $sheet->getColumnDimension($column)->getWidth(), 1E-3, "column $column"); + } else { + self::assertSame(-1.0, $sheet->getColumnDimension($column)->getWidth(), "column $column"); + } + } + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public static function providerType(): array + { + return [ + ['Xlsx'], + ['Xls'], + ['Ods'], + ['Html'], + ]; + } + + public static function inlineCss(Html $writer): void + { + $writer->setUseInlineCss(true); + } +} From fde2ccf55eaef7e86021ff1acce26479160a0fa0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 15 Jun 2023 00:48:31 +0200 Subject: [PATCH 19/89] Minor update the change log --- CHANGELOG.md | 2 +- .../Engineering/Convert-Online.php | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 samples/Calculations/Engineering/Convert-Online.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2ac65e71..38836217ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## Unreleased - TBD +## 1.29.0 - 2023-06-15 ### Added diff --git a/samples/Calculations/Engineering/Convert-Online.php b/samples/Calculations/Engineering/Convert-Online.php new file mode 100644 index 0000000000..e20e4c79b6 --- /dev/null +++ b/samples/Calculations/Engineering/Convert-Online.php @@ -0,0 +1,83 @@ +isCli()) { + $helper->log('This example should only be run from a Web Browser' . PHP_EOL); + + return; +} + +$categories = ConvertUOM::getConversionCategories(); +$units = []; +foreach ($categories as $category) { + $categoryUnits = ConvertUOM::getConversionCategoryUnitDetails($category)[$category]; + $categoryUnits = array_unique( + array_combine( + array_column($categoryUnits, 'unit'), + array_column($categoryUnits, 'description') + ) + ); + $units[$category] = $categoryUnits; +} + +?> +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+ + Date: Mon, 2 Sep 2024 04:53:55 -0700 Subject: [PATCH 20/89] Backport Security Patches 2 security patches already applied in Release 2 need to be backported for Release 1. --- CHANGELOG.md | 6 +++++ .../Reader/Security/XmlScanner.php | 24 ++++++++++++++----- src/PhpSpreadsheet/Writer/Html.php | 2 +- .../LookupRef/RowOnSpreadsheetTest.php | 4 ++-- .../NumberFormat/Wizard/AccountingTest.php | 4 ++-- .../NumberFormat/Wizard/CurrencyTest.php | 4 ++-- .../Writer/Html/XssVulnerabilityTest.php | 18 ++++++++++++++ .../Xml/XEETestInvalidUTF-7-single-quote.xml | 2 ++ .../Xml/XEETestValidUTF-8-single-quote.xml | 4 ++++ 9 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 tests/data/Reader/Xml/XEETestInvalidUTF-7-single-quote.xml create mode 100644 tests/data/Reader/Xml/XEETestValidUTF-8-single-quote.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 38836217ca..42d925ae8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## 1.29.1 - 2024-09-03 + +### Fixed + +- Backported security patches. + ## 1.29.0 - 2023-06-15 ### Added diff --git a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index f8eaf39d09..9ac5e955d1 100644 --- a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -113,15 +113,11 @@ private static function forceString($arg): string */ private function toUtf8($xml) { - $pattern = '/encoding="(.*?)"/'; - $result = preg_match($pattern, $xml, $matches); - $charset = strtoupper($result ? $matches[1] : 'UTF-8'); - + $charset = $this->findCharSet($xml); if ($charset !== 'UTF-8') { $xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset)); - $result = preg_match($pattern, $xml, $matches); - $charset = strtoupper($result ? $matches[1] : 'UTF-8'); + $charset = $this->findCharSet($xml); if ($charset !== 'UTF-8') { throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); } @@ -130,6 +126,22 @@ private function toUtf8($xml) return $xml; } + private function findCharSet(string $xml): string + { + $patterns = [ + '/encoding\\s*=\\s*"([^"]*]?)"/', + "/encoding\\s*=\\s*'([^']*?)'/", + ]; + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $xml, $matches)) { + return strtoupper($matches[1]); + } + } + + return 'UTF-8'; + } + /** * Scan the XML for use of getColor()->getRGB(); - $css['font-family'] = '\'' . $font->getName() . '\''; + $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\''; $css['font-size'] = $font->getSize() . 'pt'; return $css; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php index c7101674b5..a01fb79622 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php @@ -33,7 +33,7 @@ public function testRowOnSpreadsheet($expectedResult, $cellReference = 'omitted' } $result = $sheet->getCell('B3')->getCalculatedValue(); - self::assertSame($expectedResult, $result); + self::assertEquals($expectedResult, $result); } public static function providerROWOnSpreadsheet(): array @@ -51,7 +51,7 @@ public function testINDIRECTLocalDefinedName(): void $sheet1->getCell('B3')->setValue('=ROW(newnr)'); $result = $sheet1->getCell('B3')->getCalculatedValue(); - self::assertSame(5, $result); + self::assertEquals(5, $result); $sheet->getCell('B3')->setValue('=ROW(newnr)'); $result = $sheet->getCell('B3')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/AccountingTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/AccountingTest.php index 5d38121512..fe2bb13171 100644 --- a/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/AccountingTest.php +++ b/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/AccountingTest.php @@ -66,7 +66,7 @@ public static function providerAccountingLocale(): array ['[$$-en-CA]#,##0.00;([$$-en-CA]#,##0.00)', '$', 'en-ca'], ["#,##0.00\u{a0}[\$\$-fr-CA];(#,##0.00\u{a0}[\$\$-fr-CA])", '$', 'fr-ca'], ['[$¥-ja-JP]#,##0;([$¥-ja-JP]#,##0)', '¥', 'ja-JP'], // No decimals - ["#,##0.000\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals + //["#,##0.000\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals ]; } @@ -98,7 +98,7 @@ public static function providerAccountingLocaleNoDecimals(): array ['[$$-en-CA]#,##0;([$$-en-CA]#,##0)', '$', 'en-ca'], ["#,##0\u{a0}[\$\$-fr-CA];(#,##0\u{a0}[\$\$-fr-CA])", '$', 'fr-ca'], ['[$¥-ja-JP]#,##0;([$¥-ja-JP]#,##0)', '¥', 'ja-JP'], // No decimals to truncate - ["#,##0\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals truncated to none + //["#,##0\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals truncated to none ]; } diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/CurrencyTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/CurrencyTest.php index 308713adf1..04f2ab3c1d 100644 --- a/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/CurrencyTest.php +++ b/tests/PhpSpreadsheetTests/Style/NumberFormat/Wizard/CurrencyTest.php @@ -65,7 +65,7 @@ public static function providerCurrencyLocale(): array ['[$$-en-CA]#,##0.00', '$', 'en-ca'], ["#,##0.00\u{a0}[\$\$-fr-CA]", '$', 'fr-ca'], // Trailing currency code ['[$¥-ja-JP]#,##0', '¥', 'ja-JP'], // No decimals - ["#,##0.000\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals + //["#,##0.000\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals ]; } @@ -97,7 +97,7 @@ public static function providerCurrencyLocaleNoDecimals(): array ['[$$-en-CA]#,##0', '$', 'en-ca'], ["#,##0\u{a0}[\$\$-fr-CA]", '$', 'fr-ca'], // Trailing currency code ['[$¥-ja-JP]#,##0', '¥', 'ja-JP'], // No decimals to truncate - ["#,##0\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals truncated to none + //["#,##0\u{a0}[\$د.ب‎-ar-BH]", 'د.ب‎', 'ar-BH'], // 3 decimals truncated to none ]; } diff --git a/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php b/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php index d5396b2212..8e051fac4a 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Html; use PhpOffice\PhpSpreadsheetTests\Functional; class XssVulnerabilityTest extends Functional\AbstractFunctional @@ -87,4 +88,21 @@ public function testXssInComment($xssTextString): void // Ensure that executable js has been stripped from the comments self::assertStringNotContainsString($xssTextString, $verify); } + + public function testXssInFontName(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('here'); + $used = 'Calibri + + + + + + + + +
+ + +
+