From f68f688810c2ab447fd35ef816845d50d5878bf3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:40:52 -0800 Subject: [PATCH] Security Patch Special Characters in Protocol --- CHANGELOG.md | 7 + composer.json | 1 + composer.lock | 162 ++++++------ phpstan.neon.dist | 5 +- .../Calculation/DateTimeExcel/Helpers.php | 2 + .../Calculation/DateTimeExcel/TimeValue.php | 2 +- src/PhpSpreadsheet/Helper/Html.php | 2 +- src/PhpSpreadsheet/Reader/Html.php | 7 +- src/PhpSpreadsheet/Reader/Slk.php | 2 +- src/PhpSpreadsheet/Worksheet/Drawing.php | 2 +- .../Worksheet/MemoryDrawing.php | 2 +- src/PhpSpreadsheet/Writer/Html.php | 32 ++- src/PhpSpreadsheet/Writer/Xls/Parser.php | 249 ++++++++++++++---- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 2 +- .../Reader/Html/HtmlImage2Test.php | 81 ++++++ .../Writer/Html/BadHyperlinkTest.php | 14 +- tests/data/Reader/Xml/sec-w24f.dontuse | 65 +++++ 17 files changed, 488 insertions(+), 149 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php create mode 100644 tests/data/Reader/Xml/sec-w24f.dontuse diff --git a/CHANGELOG.md b/CHANGELOG.md index ac9c92dfd3..14a5ed7a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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). +# TBD - 2.1.8 + +### Fixed + +- Backported security patch for control characters in protocol. +- Use Composer\Pcre in Xls/Parser. Partial backport of [PR #4203](https://github.com/PHPOffice/PhpSpreadsheet/pull/4203) + # 2025-01-11 - 2.1.7 ### Deprecated diff --git a/composer.json b/composer.json index 74832919db..6f5066e989 100644 --- a/composer.json +++ b/composer.json @@ -79,6 +79,7 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", + "composer/pcre": "^3.2", "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", diff --git a/composer.lock b/composer.lock index 12100fba12..c1a26892a0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,87 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dba36d693d93eddd55314afd051fe473", + "content-hash": "74ab842c28948925489cc00253e63c3f", "packages": [ + { + "name": "composer/pcre", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.8" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.2.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-07-25T09:36:02+00:00" + }, { "name": "maennchen/zipstream-php", "version": "2.4.0", @@ -467,77 +546,6 @@ } ], "packages-dev": [ - { - "name": "composer/pcre", - "version": "3.1.3", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-03-19T10:26:25+00:00" - }, { "name": "composer/semver", "version": "3.4.0", @@ -1726,16 +1734,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.67", + "version": "1.11.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" + "reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec", + "reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec", "shasum": "" }, "require": { @@ -1780,7 +1788,7 @@ "type": "github" } ], - "time": "2024-04-16T07:22:02+00:00" + "time": "2024-07-24T07:01:22+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e6d7d0d33c..e137cb883a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,6 +2,7 @@ includes: - phpstan-baseline.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/composer/pcre/extension.neon parameters: level: 8 @@ -22,9 +23,9 @@ parameters: - src/PhpSpreadsheet/Writer/ZipStream3.php parallel: processTimeout: 300.0 - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.generics # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' - '~^Variable \$helper might not be defined\.$~' diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php index 94ffa4c4d3..1e9af6cb5a 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -19,6 +19,8 @@ class Helpers */ public static function isLeapYear(int|string $year): bool { + $year = (int) $year; + return (($year % 4) === 0) && (($year % 100) !== 0) || (($year % 400) === 0); } diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php index 0af421143f..f744e30694 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -52,7 +52,7 @@ public static function fromString(null|array|string|int|bool|float $timeValue): $arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: []; if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) { - $arraySplit[0] = ($arraySplit[0] % 24); + $arraySplit[0] = ($arraySplit[0] % 24); // @phpstan-ignore-line $timeValue = implode(':', $arraySplit); } diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index a0d272c77d..c33715af57 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -693,7 +693,7 @@ private function rgbToColour(string $rgbValue): string { preg_match_all('/\d+/', $rgbValue, $values); foreach ($values[0] as &$value) { - $value = str_pad(dechex($value), 2, '0', STR_PAD_LEFT); + $value = str_pad(dechex($value), 2, '0', STR_PAD_LEFT); // @phpstan-ignore-line } return implode('', $values[0]); diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 6b4dd997fa..d8f117b2c2 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -170,7 +170,7 @@ private function readBeginning(): string private function readEnding(): string { $meta = stream_get_meta_data($this->fileHandle); - $filename = $meta['uri']; + $filename = $meta['uri']; // @phpstan-ignore-line $size = (int) filesize($filename); if ($size === 0) { @@ -1031,7 +1031,10 @@ private function insertImage(Worksheet $sheet, string $column, int $row, array $ $name = $attributes['alt'] ?? null; $drawing = new Drawing(); - $drawing->setPath($src); + $drawing->setPath($src, false); + if ($drawing->getPath() === '') { + return; + } $drawing->setWorksheet($sheet); $drawing->setCoordinates($column . $row); $drawing->setOffsetX(0); diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index e56cb8ebc0..775e5e8bb1 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -447,7 +447,7 @@ private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void private function processPColors(string $rowDatum, array &$formatArray): void { if (preg_match('/L([1-9]\\d*)/', $rowDatum, $matches)) { - $fontColor = $matches[1] % 8; + $fontColor = $matches[1] % 8; // @phpstan-ignore-line $formatArray['font']['color']['argb'] = self::COLOR_ARRAY[$fontColor]; } } diff --git a/src/PhpSpreadsheet/Worksheet/Drawing.php b/src/PhpSpreadsheet/Worksheet/Drawing.php index 55450dbfe5..5905d7f2fe 100644 --- a/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -103,7 +103,7 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip $this->path = ''; // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - if (filter_var($path, FILTER_VALIDATE_URL)) { + if (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\\w\\s\\x00-\\x1f]+):/u', $path) && !preg_match('/^([\\w]+):/u', $path))) { if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } diff --git a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index fa6157db15..e6d4026f26 100644 --- a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -102,7 +102,7 @@ private function cloneResource(): void $transparent = imagecolortransparent($this->imageResource); if ($transparent >= 0) { $rgb = imagecolorsforindex($this->imageResource, $transparent); - if (empty($rgb)) { + if (empty($rgb)) { // @phpstan-ignore-line throw new Exception('Could not get image colors'); } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index d448fa7925..19ca5f4eb0 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; @@ -621,13 +622,13 @@ private function writeImageInCell(string $coordinates): string $filename = $drawing->getPath(); // Strip off eventual '.' - $filename = (string) preg_replace('/^[.]/', '', $filename); + $filename = Preg::replace('/^[.]/', '', $filename); // Prepend images root $filename = $this->getImagesRoot() . $filename; // Strip off eventual '.' if followed by non-/ - $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename); + $filename = Preg::replace('@^[.]([^/])@', '$1', $filename); // Convert UTF8 data to PCDATA $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); @@ -1350,7 +1351,7 @@ private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cel // Converts the cell content so that spaces occuring at beginning of each new line are replaced by   // Example: " Hello\n to the world" is converted to "  Hello\n to the world" - $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); + $cellData = Preg::replace('/(?m)(?:^|\\G) /', ' ', $cellData); // convert newline "\n" to '
' $cellData = nl2br($cellData); @@ -1495,10 +1496,11 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { $url = $worksheet->getHyperlink($coordinate)->getUrl(); $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - $urlTrim = preg_replace('/^\\s+/u', '', $urlDecode1) ?? $urlDecode1; - $parseScheme = preg_match('/^([\\w\\s]+):/u', strtolower($urlTrim), $matches); - if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) { + $urlTrim = Preg::replace('/^\\s+/u', '', $urlDecode1); + $parseScheme = Preg::isMatch('/^([\\w\\s\\x00-\\x1f]+):/u', strtolower($urlTrim), $matches); + if ($parseScheme && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 'mailto', 's3'], true)) { $cellData = htmlspecialchars($url, Settings::htmlEntityFlags()); + $cellData = self::replaceControlChars($cellData); } else { $cellData = '' . $cellData . ''; } @@ -1540,6 +1542,20 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri return $html; } + public static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + + private static function replaceControlChars(string $convert): string + { + return Preg::replaceCallback( + '/[\\x00-\\x1f]/', + [self::class, 'replaceNonAscii'], + $convert + ); + } + /** * Takes array where of CSS properties / values and converts to CSS string. */ @@ -1627,8 +1643,8 @@ public function formatColor(string $value, string $format): string $matches = []; $color_regex = '/^\\[[a-zA-Z]+\\]/'; - if (preg_match($color_regex, $format, $matches)) { - $color = str_replace(['[', ']'], '', $matches[0]); + if (Preg::isMatch($color_regex, $format, $matches)) { + $color = str_replace(['[', ']'], '', $matches[0]); // @phpstan-ignore-line $color = strtolower($color); } diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index c51b490c92..c0f60d66e1 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xls; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -45,7 +46,26 @@ class Parser // Invalid sheet title characters cannot occur in the sheet title: // *:/\?[] (usual invalid sheet title characters) // Single quote is represented as a pair '' - const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+'; + // Former value for this constant led to "catastrophic backtracking", + // unable to handle double apostrophes. + // (*COMMIT) should prevent this. + const REGEX_SHEET_TITLE_QUOTED = "([^*:/\\\\?\[\]']|'')+"; + + const REGEX_CELL_TITLE_QUOTED = "~^'" + . self::REGEX_SHEET_TITLE_QUOTED + . '(:' . self::REGEX_SHEET_TITLE_QUOTED . ')?' + . "'!(*COMMIT)" + . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)' + . '$~u'; + + const REGEX_RANGE_TITLE_QUOTED = "~^'" + . self::REGEX_SHEET_TITLE_QUOTED + . '(:' . self::REGEX_SHEET_TITLE_QUOTED . ')?' + . "'!(*COMMIT)" + . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)' + . ':' + . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)' + . '$~u'; /** * The index of the character we are currently looking at. @@ -478,34 +498,34 @@ public function __construct(Spreadsheet $spreadsheet) */ private function convert(string $token): string { - if (preg_match('/"([^"]|""){0,255}"/', $token)) { + if (Preg::isMatch('/"([^"]|""){0,255}"/', $token)) { return $this->convertString($token); } if (is_numeric($token)) { return $this->convertNumber($token); } // match references like A1 or $A$1 - if (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { + if (Preg::isMatch('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { return $this->convertRef2d($token); } // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 - if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { + if (Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { return $this->convertRef3d($token); } // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 - if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) { + if (self::matchCellSheetnameQuoted($token)) { return $this->convertRef3d($token); } // match ranges like A1:B2 or $A$1:$B$2 - if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { + if (Preg::isMatch('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { return $this->convertRange2d($token); } // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 - if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { + if (Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { return $this->convertRange3d($token); } // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 - if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) { + if (self::matchRangeSheetnameQuoted($token)) { return $this->convertRange3d($token); } // operators (including parentheses) @@ -513,14 +533,14 @@ private function convert(string $token): string return pack('C', $this->ptg[$token]); } // match error codes - if (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { + if (Preg::isMatch('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { return $this->convertError($token); } - if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { + if (Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { return $this->convertDefinedName($token); } // commented so argument number can be processed correctly. See toReversePolish(). - /*if (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) + /*if (Preg::isMatch("/[A-Z0-9\xc0-\xdc\.]+/", $token)) { return($this->convertFunction($token, $this->_func_args)); }*/ @@ -528,10 +548,10 @@ private function convert(string $token): string if ($token == 'arg') { return ''; } - if (preg_match('/^true$/i', $token)) { + if (Preg::isMatch('/^true$/i', $token)) { return $this->convertBool(1); } - if (preg_match('/^false$/i', $token)) { + if (Preg::isMatch('/^false$/i', $token)) { return $this->convertBool(0); } @@ -542,18 +562,18 @@ private function convert(string $token): string /** * Convert a number token to ptgInt or ptgNum. * - * @param mixed $num an integer or double for conversion to its ptg value + * @param float|int|string $num an integer or double for conversion to its ptg value */ private function convertNumber(mixed $num): string { // Integer in the range 0..2**16-1 - if ((preg_match('/^\\d+$/', $num)) && ($num <= 65535)) { + if ((Preg::isMatch('/^\\d+$/', (string) $num)) && ($num <= 65535)) { return pack('Cv', $this->ptg['ptgInt'], $num); } // A float if (BIFFwriter::getByteOrder()) { // if it's Big Endian - $num = strrev($num); + $num = strrev((string) $num); } return pack('Cd', $this->ptg['ptgNum'], $num); @@ -613,7 +633,7 @@ private function convertRange2d(string $range, int $class = 0): string { // TODO: possible class value 0,1,2 check Formula.pm // Split the range into 2 cell refs - if (preg_match('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { + if (Preg::isMatch('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) { [$cell1, $cell2] = explode(':', $range); } else { // TODO: use real error codes @@ -658,7 +678,7 @@ private function convertRange3d(string $token): string [$cell1, $cell2] = explode(':', $range ?? ''); // Convert the cell references - if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) { + if (Preg::isMatch('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) { [$row1, $col1] = $this->cellToPackedRowcol($cell1); [$row2, $col2] = $this->cellToPackedRowcol($cell2); } else { // It's a rows range (like 26:27) @@ -774,11 +794,11 @@ private function convertDefinedName(string $name): string */ private function getRefIndex(string $ext_ref): string { - $ext_ref = (string) preg_replace(["/^'/", "/'$/"], ['', ''], $ext_ref); // Remove leading and trailing ' if any. + $ext_ref = Preg::replace(["/^'/", "/'$/"], ['', ''], $ext_ref); // Remove leading and trailing ' if any. $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with ' // Check if there is a sheet range eg., Sheet1:Sheet2. - if (preg_match('/:/', $ext_ref)) { + if (Preg::isMatch('/:/', $ext_ref)) { [$sheet_name1, $sheet_name2] = explode(':', $ext_ref); $sheet1 = $this->getSheetIndex($sheet_name1); @@ -894,7 +914,11 @@ private function cellToPackedRowcol(string $cell): array */ private function rangeToPackedRange(string $range): array { - preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match); + if (!Preg::isMatch('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match)) { + // @codeCoverageIgnoreStart + throw new WriterException('Regexp failure in rangeToPackedRange'); + // @codeCoverageIgnoreEnd + } // return absolute rows if there is a $ in the ref $row1_rel = empty($match[1]) ? 1 : 0; $row1 = $match[2]; @@ -933,7 +957,11 @@ private function rangeToPackedRange(string $range): array */ private function cellToRowcol(string $cell): array { - preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match); + if (!Preg::isMatch('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match)) { + // @codeCoverageIgnoreStart + throw new WriterException('Regexp failure in cellToRowcol'); + // @codeCoverageIgnoreEnd + } // return absolute column if there is a $ in the ref $col_rel = empty($match[1]) ? 1 : 0; $col_ref = $match[2]; @@ -941,11 +969,11 @@ private function cellToRowcol(string $cell): array $row = $match[4]; // Convert base26 column string to a number. - $expn = strlen($col_ref) - 1; + $expn = strlen($col_ref) - 1; // @phpstan-ignore-line $col = 0; - $col_ref_length = strlen($col_ref); + $col_ref_length = strlen($col_ref); // @phpstan-ignore-line for ($i = 0; $i < $col_ref_length; ++$i) { - $col += (ord($col_ref[$i]) - 64) * 26 ** $expn; + $col += (ord($col_ref[$i]) - 64) * 26 ** $expn; // @phpstan-ignore-line --$expn; } @@ -1045,52 +1073,109 @@ private function match(string $token): string } // if it's a reference A1 or $A$1 or $A1 or A$1 - if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) { + if ( + Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) + && !Preg::isMatch('/\d/', $this->lookAhead) + && ($this->lookAhead !== ':') + && ($this->lookAhead !== '.') + && ($this->lookAhead !== '!') + ) { return $token; } // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) - if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { + if ( + Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) + && !Preg::isMatch('/\d/', $this->lookAhead) + && ($this->lookAhead !== ':') + && ($this->lookAhead !== '.') + ) { return $token; } // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) - if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { + if ( + self::matchCellSheetnameQuoted($token) + && !Preg::isMatch('/\\d/', $this->lookAhead) + && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') + ) { return $token; } // if it's a range A1:A2 or $A$1:$A$2 - if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) { + if ( + Preg::isMatch( + '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', + $token + ) + && !Preg::isMatch('/\d/', $this->lookAhead) + ) { return $token; } // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 - if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) { + if ( + Preg::isMatch( + '/^' + . self::REGEX_SHEET_TITLE_UNQUOTED + . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED + . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', + $token + ) + && !Preg::isMatch('/\d/', $this->lookAhead) + ) { return $token; } // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 - if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) { + if ( + self::matchRangeSheetnameQuoted($token) + && !Preg::isMatch('/\\d/', $this->lookAhead) + ) { return $token; } // If it's a number (check that it's not a sheet name or range) if (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { return $token; } - if (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) { + if ( + Preg::isMatch('/"([^"]|""){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 - if (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') { + if ( + Preg::isMatch('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) + || $token === '#N/A' + ) { return $token; } // if it's a function call - if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) { + if ( + Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) + && ($this->lookAhead === '(') + ) { return $token; } - if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) { + if ( + Preg::isMatch( + '/^' + . Calculation::CALCULATION_REGEXP_DEFINEDNAME + . '$/miu', + $token + ) + && $this->spreadsheet->getDefinedName($token) !== null + ) { return $token; } - if (preg_match('/^true$/i', $token) && ($this->lookAhead === ')' || $this->lookAhead === ',')) { + if ( + Preg::isMatch('/^true$/i', $token) + && ($this->lookAhead === ')' || $this->lookAhead === ',') + ) { return $token; } - if (preg_match('/^false$/i', $token) && ($this->lookAhead === ')' || $this->lookAhead === ',')) { + if ( + Preg::isMatch('/^false$/i', $token) + && ($this->lookAhead === ')' || $this->lookAhead === ',') + ) { return $token; } if (str_ends_with($token, ')')) { @@ -1172,7 +1257,7 @@ private function condition(): array private function expression(): array { // If it's a string return a string node - if (preg_match('/"([^"]|""){0,255}"/', $this->currentToken)) { + if (Preg::isMatch('/"([^"]|""){0,255}"/', $this->currentToken)) { $tmp = str_replace('""', '"', $this->currentToken); if (($tmp == '"') || ($tmp == '')) { // Trap for "" that has been used for an empty string @@ -1182,12 +1267,17 @@ private function expression(): array $this->advance(); return $result; - } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') { // error code + } + if ( + Preg::isMatch('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) + || $this->currentToken == '#N/A' + ) { // error code $result = $this->createTree($this->currentToken, 'ptgErr', ''); $this->advance(); return $result; - } elseif ($this->currentToken == '-') { // negative value + } + if ($this->currentToken == '-') { // negative value // catch "-" Term $this->advance(); $result2 = $this->expression(); @@ -1293,20 +1383,28 @@ private function fact(): array return $result; } // if it's a reference - if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) { + if (Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) { $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } - if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) { + if ( + Preg::isMatch( + '/^' + . self::REGEX_SHEET_TITLE_UNQUOTED + . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED + . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', + $this->currentToken + ) + ) { // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; } - if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) { + if (self::matchCellSheetnameQuoted($this->currentToken)) { // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) $result = $this->createTree($this->currentToken, '', ''); $this->advance(); @@ -1314,8 +1412,14 @@ private function fact(): array return $result; } if ( - preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) - || preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) + Preg::isMatch( + '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', + $this->currentToken + ) + || Preg::isMatch( + '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', + $this->currentToken + ) ) { // if it's a range A1:B2 or $A$1:$B$2 // must be an error? @@ -1324,7 +1428,16 @@ private function fact(): array return $result; } - if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) { + if ( + Preg::isMatch( + '/^' + . self::REGEX_SHEET_TITLE_UNQUOTED + . '(\\:' + . self::REGEX_SHEET_TITLE_UNQUOTED + . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', + $this->currentToken + ) + ) { // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2) // must be an error? $result = $this->createTree($this->currentToken, '', ''); @@ -1332,7 +1445,7 @@ private function fact(): array return $result; } - if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) { + if (self::matchRangeSheetnameQuoted($this->currentToken)) { // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2) // must be an error? $result = $this->createTree($this->currentToken, '', ''); @@ -1352,17 +1465,28 @@ private function fact(): array return $result; } - if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) { + if ( + Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) + && ($this->lookAhead === '(') + ) { // if it's a function call return $this->func(); } - if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) { + if ( + Preg::isMatch( + '/^' + . Calculation::CALCULATION_REGEXP_DEFINEDNAME + . '$/miu', + $this->currentToken + ) + && $this->spreadsheet->getDefinedName($this->currentToken) !== null + ) { $result = $this->createTree('ptgName', $this->currentToken, ''); $this->advance(); return $result; } - if (preg_match('/^true|false$/i', $this->currentToken)) { + if (Preg::isMatch('/^true|false$/i', $this->currentToken)) { $result = $this->createTree($this->currentToken, '', ''); $this->advance(); @@ -1483,9 +1607,12 @@ public function toReversePolish(array $tree = []): string } // if it's a function convert it here (so we can set it's arguments) if ( - preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) - && !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) - && !preg_match('/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', $tree['value']) + Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) + && !Preg::isMatch('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) + && !Preg::isMatch( + '/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/', + $tree['value'] + ) && !is_numeric($tree['value']) && !isset($this->ptg[$tree['value']]) ) { @@ -1503,4 +1630,20 @@ public function toReversePolish(array $tree = []): string return $polish . $converted_tree; } + + public static function matchCellSheetnameQuoted(string $token): bool + { + return Preg::isMatch( + self::REGEX_CELL_TITLE_QUOTED, + $token + ); + } + + public static function matchRangeSheetnameQuoted(string $token): bool + { + return Preg::isMatch( + self::REGEX_RANGE_TITLE_QUOTED, + $token + ); + } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index dadfc7003e..a2f33985a5 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2374,7 +2374,7 @@ public function processBitmap(string $bitmap): array } // Slurp the file into a string. - $data = (string) fread($bmp_fd, (int) filesize($bitmap)); + $data = (string) fread($bmp_fd, (int) filesize($bitmap)); // @phpstan-ignore-line // Check that the file is big enough to be a bitmap. if (strlen($data) <= 0x36) { diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php new file mode 100644 index 0000000000..c2d610ff6b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php @@ -0,0 +1,81 @@ + + + test image voilà + + '; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + + /** @var Drawing $drawing */ + $drawing = $firstSheet->getDrawingCollection()[0]; + self::assertEquals($imagePath, $drawing->getPath()); + self::assertEquals('A1', $drawing->getCoordinates()); + } + + public function testCantInsertImageNotFound(): void + { + if (getenv('SKIP_URL_IMAGE_TEST') === '1') { + self::markTestSkipped('Skipped due to setting of environment variable'); + } + $imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/xxx01-03-filter-icon-1.png'; + $html = ' + + + +
test image voilà
'; + $filename = HtmlHelper::createHtml($html); + $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + $firstSheet = $spreadsheet->getSheet(0); + $drawingCollection = $firstSheet->getDrawingCollection(); + self::assertCount(0, $drawingCollection); + } + + /** + * @dataProvider providerBadProtocol + */ + public function testCannotInsertImageBadProtocol(string $imagePath): void + { + $this->expectException(SpreadsheetException::class); + $this->expectExceptionMessage('Invalid protocol for linked drawing'); + $html = ' + + + +
test image voilà
'; + $filename = HtmlHelper::createHtml($html); + HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); + } + + public static function providerBadProtocol(): array + { + return [ + 'unknown protocol' => ['httpx://example.com/image.png'], + 'embedded whitespace' => ['ht tp://example.com/image.png'], + 'control character' => ["\x14http://example.com/image.png"], + 'mailto' => ['mailto:xyz@example.com'], + 'mailto whitespace' => ['mail to:xyz@example.com'], + 'phar' => ['phar://example.com/image.phar'], + 'phar control' => ["\x14phar://example.com/image.phar"], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php index 669594bb1c..6e2634c6cf 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Html; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Reader\Xml as XmlReader; use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter; use PHPUnit\Framework\TestCase; @@ -17,7 +18,18 @@ public function testBadHyperlink(): void $spreadsheet = $reader->load($infile); $writer = new HtmlWriter($spreadsheet); $html = $writer->generateHtmlAll(); - self::assertStringContainsString("jav\tascript:alert()", $html); + self::assertStringContainsString('jav ascript:alert()', $html); + $spreadsheet->disconnectWorksheets(); + } + + public function testControlCharacter(): void + { + $reader = new XmlReader(); + $infile = 'tests/data/Reader/Xml/sec-w24f.dontuse'; + $spreadsheet = $reader->load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('j avascript:alert(1)', $html); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/data/Reader/Xml/sec-w24f.dontuse b/tests/data/Reader/Xml/sec-w24f.dontuse new file mode 100644 index 0000000000..f39faa4fc8 --- /dev/null +++ b/tests/data/Reader/Xml/sec-w24f.dontuse @@ -0,0 +1,65 @@ + + + + + author + author + 2015-06-05T18:19:34Z + 2024-12-25T10:16:07Z + 16.00 + + + + + + 11020 + 19420 + 32767 + 32767 + False + False + + + + + + + + + + + +
+ + +
+