diff --git a/CHANGELOG.md b/CHANGELOG.md index f686f3e661..b5e5b419b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Ods Reader Sheet Names with Period. [Issue #4311](https://github.com/PHPOffice/PhpSpreadsheet/issues/4311) [PR #4313](https://github.com/PHPOffice/PhpSpreadsheet/pull/4313) - Mpdf and Tcpdf Hidden Columns and Merged Cells. [Issue #4319](https://github.com/PHPOffice/PhpSpreadsheet/issues/4319) [PR #4320](https://github.com/PHPOffice/PhpSpreadsheet/pull/4320) - Html Writer Allow mailto. [Issue #4316](https://github.com/PHPOffice/PhpSpreadsheet/issues/4316) [PR #4322](https://github.com/PHPOffice/PhpSpreadsheet/pull/4322) +- Use composer/pcre rather than preg_* in Writer. [PR #4323](https://github.com/PHPOffice/PhpSpreadsheet/pull/4323) ## 2025-01-11 - 3.8.0 diff --git a/composer.json b/composer.json index 7f1777676c..22d22c1a67 100644 --- a/composer.json +++ b/composer.json @@ -79,6 +79,7 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", + "composer/pcre": "^3.3", "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", diff --git a/composer.lock b/composer.lock index cc706e608e..ee9da8ac4f 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": "f7caa2101e268445a358275ee83b90d9", + "content-hash": "264ef911bd6d773a3a9085bc86b0ea48", "packages": [ + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "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.3.2" + }, + "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-11-12T16:29:46+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.1.1", @@ -467,85 +546,6 @@ ], "time": "2022-12-23T10:58:28+00:00" }, - { - "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": { - "branch-alias": { - "dev-main": "3.x-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "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": "composer/semver", "version": "3.4.2", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 06fbbcdd66..e34d429456 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 diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index 38ba4d9c45..e9d3402590 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -1,5 +1,6 @@ EOF; - return preg_replace($bodystring, $bodyrepl, $html) ?? ''; + return preg_replace($bodystring, $bodyrepl, $html) ?? throw new SpreadsheetException('preg failed'); } require __DIR__ . '/../Header.php'; diff --git a/samples/Pdf/21c_Pdf.php b/samples/Pdf/21c_Pdf.php index 5a6a26d379..66d964aedd 100644 --- a/samples/Pdf/21c_Pdf.php +++ b/samples/Pdf/21c_Pdf.php @@ -1,5 +1,6 @@ /'; $simulatedBodyStart = Mpdf::SIMULATED_BODY_START; $bodyrepl = <<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()); @@ -1411,7 +1412,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); @@ -1599,9 +1600,9 @@ 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', 'mailto', 's3'], true)) { + $urlTrim = Preg::replace('/^\\s+/u', '', $urlDecode1); + $parseScheme = Preg::isMatch('/^([\\w\\s]+):/u', strtolower($urlTrim), $matches); + if ($parseScheme && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 'mailto', 's3'], true)) { $cellData = htmlspecialchars($url, Settings::htmlEntityFlags()); } else { $tooltip = $worksheet->getHyperlink($coordinate)->getTooltip(); @@ -1755,7 +1756,7 @@ public static function formatColorStatic(string $value, string $format): string $matches = []; $color_regex = '/^\\[[a-zA-Z]+\\]/'; - if (preg_match($color_regex, $format, $matches)) { + if (Preg::isMatch($color_regex, $format, $matches)) { $color = str_replace(['[', ']'], '', $matches[0]); $color = strtolower($color); } diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 3b9cc82458..1c87385c57 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; @@ -227,16 +228,15 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void } } if (isset($attributes['ref'])) { - if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', (string) $attributes['ref'], $matches) == 1) { + if (Preg::isMatch('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', (string) $attributes['ref'], $matches)) { $matrixRowSpan = 1; $matrixColSpan = 1; if (isset($matches[3])) { $minRow = (int) $matches[2]; - // https://github.com/phpstan/phpstan/issues/11602 - $maxRow = (int) $matches[5]; // @phpstan-ignore-line + $maxRow = (int) $matches[5]; $matrixRowSpan = $maxRow - $minRow + 1; $minCol = Coordinate::columnIndexFromString($matches[1]); - $maxCol = Coordinate::columnIndexFromString($matches[4]); // @phpstan-ignore-line + $maxCol = Coordinate::columnIndexFromString($matches[4]); $matrixColSpan = $maxCol - $minCol + 1; } $objWriter->writeAttribute('table:number-matrix-columns-spanned', "$matrixColSpan"); diff --git a/src/PhpSpreadsheet/Writer/Ods/Formula.php b/src/PhpSpreadsheet/Writer/Ods/Formula.php index 335df56dd6..4a874c7b15 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Formula.php +++ b/src/PhpSpreadsheet/Writer/Ods/Formula.php @@ -2,8 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\DefinedName; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Formula { @@ -33,14 +35,13 @@ public function convertFormula(string $formula, string $worksheetName = ''): str private function convertDefinedNames(string $formula): string { - $splitCount = preg_match_all( + $splitCount = Preg::matchAllWithOffsets( '/' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '/mui', $formula, - $splitRanges, - PREG_OFFSET_CAPTURE + $splitRanges ); - $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0)); $offsets = array_column($splitRanges[0], 1); $values = array_column($splitRanges[0], 0); @@ -60,14 +61,13 @@ private function convertDefinedNames(string $formula): string private function convertCellReferences(string $formula, string $worksheetName): string { - $splitCount = preg_match_all( + $splitCount = Preg::matchAllWithOffsets( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', $formula, - $splitRanges, - PREG_OFFSET_CAPTURE + $splitRanges ); - $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0)); $offsets = array_column($splitRanges[0], 1); $worksheets = $splitRanges[2]; @@ -76,7 +76,7 @@ private function convertCellReferences(string $formula, string $worksheetName): // Replace any commas in the formula with semi-colons for Ods // If by chance there are commas in worksheet names, then they will be "fixed" again in the loop - // because we've already extracted worksheet names with our preg_match_all() + // because we've already extracted worksheet names with our Preg::matchAllWithOffsets() $formula = str_replace(',', ';', $formula); while ($splitCount > 0) { --$splitCount; diff --git a/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php b/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php index 0fda160f31..3e622f20d2 100644 --- a/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php +++ b/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php @@ -2,8 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\DefinedName; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -80,14 +82,13 @@ private function writeNamedRange(DefinedName $definedName): void private function convertAddress(DefinedName $definedName, string $address): string { - $splitCount = preg_match_all( + $splitCount = Preg::matchAllWithOffsets( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', $address, - $splitRanges, - PREG_OFFSET_CAPTURE + $splitRanges ); - $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0)); $offsets = array_column($splitRanges[0], 1); $worksheets = $splitRanges[2]; diff --git a/src/PhpSpreadsheet/Writer/Ods/Settings.php b/src/PhpSpreadsheet/Writer/Ods/Settings.php index cfe1b598d9..695c604784 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Settings.php +++ b/src/PhpSpreadsheet/Writer/Ods/Settings.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; @@ -97,7 +98,7 @@ private function writeWorksheetSettings(XMLWriter $objWriter, Worksheet $workshe private function writeSelectedCells(XMLWriter $objWriter, Worksheet $worksheet): void { $selected = $worksheet->getSelectedCells(); - if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { + if (Preg::isMatch('/^([a-z]+)([0-9]+)/i', $selected, $matches)) { $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; $rowSel = (int) $matches[2] - 1; $objWriter->startElement('config:config-item'); diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 422492d4e8..52b4ad3538 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); } @@ -547,7 +567,7 @@ private function convert(string $token): string private function convertNumber(mixed $num): string { // Integer in the range 0..2**16-1 - if ((preg_match('/^\\d+$/', (string) $num)) && ($num <= 65535)) { + if ((Preg::isMatch('/^\\d+$/', (string) $num)) && ($num <= 65535)) { return pack('Cv', $this->ptg['ptgInt'], $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,7 @@ private function cellToPackedRowcol(string $cell): array */ private function rangeToPackedRange(string $range): array { - if (preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match) !== 1) { + if (!Preg::isMatch('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match)) { // @codeCoverageIgnoreStart throw new WriterException('Regexp failure in rangeToPackedRange'); // @codeCoverageIgnoreEnd @@ -937,7 +957,7 @@ private function rangeToPackedRange(string $range): array */ private function cellToRowcol(string $cell): array { - if (preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match) !== 1) { + if (!Preg::isMatch('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match)) { // @codeCoverageIgnoreStart throw new WriterException('Regexp failure in cellToRowcol'); // @codeCoverageIgnoreEnd @@ -1053,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, ')')) { @@ -1180,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 @@ -1190,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(); @@ -1301,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(); @@ -1322,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? @@ -1332,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, '', ''); @@ -1340,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, '', ''); @@ -1360,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(); @@ -1491,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']]) ) { @@ -1511,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/Workbook.php b/src/PhpSpreadsheet/Writer/Xls/Workbook.php index c2b4496a53..43e9f1cd77 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xls/Workbook.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xls; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\DefinedName; @@ -508,14 +509,13 @@ private function writeAllStyles(): void private function parseDefinedNameValue(DefinedName $definedName): string { $definedRange = $definedName->getValue(); - $splitCount = preg_match_all( + $splitCount = Preg::matchAllWithOffsets( '/' . Calculation::CALCULATION_REGEXP_CELLREF . '/mui', $definedRange, - $splitRanges, - PREG_OFFSET_CAPTURE + $splitRanges ); - $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0)); $offsets = array_column($splitRanges[0], 1); $worksheets = $splitRanges[2]; diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 5e3aa0b820..743f46d960 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xls; +use Composer\Pcre\Preg; use GdImage; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; @@ -461,11 +462,11 @@ public function close(): void if (str_contains($url, 'sheet://')) { // internal to current workbook $url = str_replace('sheet://', 'internal:', $url); - } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) { + } elseif (Preg::isMatch('/^(http:|https:|ftp:|mailto:)/', $url)) { // URL - } elseif (!empty($hyperlinkbase) && preg_match('~^([A-Za-z]:)?[/\\\\]~', $url) !== 1) { + } elseif (!empty($hyperlinkbase) && !Preg::isMatch('~^([A-Za-z]:)?[/\\\\]~', $url)) { $url = "$hyperlinkbase$url"; - if (preg_match('/^(http:|https:|ftp:|mailto:)/', $url) !== 1) { + if (!Preg::isMatch('/^(http:|https:|ftp:|mailto:)/', $url)) { $url = 'external:' . $url; } } else { @@ -933,10 +934,10 @@ private function writeUrl(int $row, int $col, string $url): void private function writeUrlRange(int $row1, int $col1, int $row2, int $col2, string $url): void { // Check for internal/external sheet links or default to web link - if (preg_match('[^internal:]', $url)) { + if (Preg::isMatch('[^internal:]', $url)) { $this->writeUrlInternal($row1, $col1, $row2, $col2, $url); } - if (preg_match('[^external:]', $url)) { + if (Preg::isMatch('[^external:]', $url)) { $this->writeUrlExternal($row1, $col1, $row2, $col2, $url); } @@ -969,8 +970,7 @@ public function writeUrlWeb(int $row1, int $col1, int $row2, int $col2, string $ // Convert URL to a null terminated wchar string - /** @phpstan-ignore-next-line */ - $url = implode("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY)); + $url = implode("\0", Preg::split("''", $url, -1, PREG_SPLIT_NO_EMPTY)); $url = $url . "\0\0\0"; // Pack the length of the URL @@ -1003,7 +1003,7 @@ private function writeUrlInternal(int $row1, int $col1, int $row2, int $col2, st $record = 0x01B8; // Record identifier // Strip URL type - $url = (string) preg_replace('/^internal:/', '', $url); + $url = Preg::replace('/^internal:/', '', $url); // Pack the undocumented parts of the hyperlink stream $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); @@ -1050,7 +1050,7 @@ private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, st { // Network drives are different. We will handle them separately // MS/Novell network drives and shares start with \\ - if (preg_match('[^external:\\\\]', $url)) { + if (Preg::isMatch('[^external:\\\\]', $url)) { return; } @@ -1058,7 +1058,7 @@ private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, st // Strip URL type and change Unix dir separator to Dos style (if needed) // - $url = (string) preg_replace(['/^external:/', '/\//'], ['', '\\'], $url); + $url = Preg::replace(['/^external:/', '/\//'], ['', '\\'], $url); // Determine if the link is relative or absolute: // relative if link contains no dir separator, "somefile.xls" @@ -1066,7 +1066,7 @@ private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, st // otherwise, absolute $absolute = 0x00; // relative path - if (preg_match('/^[A-Z]:/', $url)) { + if (Preg::isMatch('/^[A-Z]:/', $url)) { $absolute = 0x02; // absolute path on Windows, e.g. C:\... } $link_type = 0x01 | $absolute; @@ -1075,7 +1075,7 @@ private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, st // parameters accordingly. // Split the dir name and sheet name (if it exists) $dir_long = $url; - if (preg_match('/\\#/', $url)) { + if (Preg::isMatch('/\\#/', $url)) { $link_type |= 0x08; } @@ -1083,11 +1083,11 @@ private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, st $link_type = pack('V', $link_type); // Calculate the up-level dir count e.g.. (..\..\..\ == 3) - $up_count = preg_match_all('/\\.\\.\\\\/', $dir_long, $useless); + $up_count = Preg::isMatchAll('/\\.\\.\\\\/', $dir_long, $useless); $up_count = pack('v', $up_count); // Store the short dos dir name (null terminated) - $dir_short = (string) preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; + $dir_short = Preg::replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; // Store the long dir name as a wchar string (non-null terminated) //$dir_long = $dir_long . "\0"; @@ -2640,7 +2640,7 @@ private function writeDataValidity(): void $options |= $errorStyle << 4; // explicit formula? - if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) { + if ($type == 0x03 && Preg::isMatch('/^\".*\"$/', $dataValidation->getFormula1())) { $options |= 0x01 << 7; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index 4dac1855e8..f8a70ea55a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -2,10 +2,12 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use Composer\Pcre\Preg; use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\DefinedName; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as ActualWorksheet; @@ -192,14 +194,13 @@ private function writeNamedRangeForPrintArea(ActualWorksheet $worksheet, int $wo private function getDefinedRange(DefinedName $definedName): string { $definedRange = $definedName->getValue(); - $splitCount = preg_match_all( + $splitCount = Preg::matchAllWithOffsets( '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', $definedRange, - $splitRanges, - PREG_OFFSET_CAPTURE + $splitRanges ); - $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $lengths = array_map([StringHelper::class, 'strlenAllowNull'], array_column($splitRanges[0], 0)); $offsets = array_column($splitRanges[0], 1); $worksheets = $splitRanges[2]; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 8a17eb9f16..ed441b1686 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces; use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; @@ -507,7 +508,7 @@ public function writeVMLHeaderFooterImages(\PhpOffice\PhpSpreadsheet\Worksheet\W private function writeVMLHeaderFooterImage(XMLWriter $objWriter, string $reference, HeaderFooterDrawing $image): void { // Calculate object id - if (preg_match('{(\d+)}', md5($reference), $m) !== 1) { + if (!Preg::isMatch('{(\d+)}', md5($reference), $m)) { // @codeCoverageIgnoreStart throw new WriterException('Regexp failure in writeVMLHeaderFooterImage'); // @codeCoverageIgnoreEnd diff --git a/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php b/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php index 16834dc04d..31e1e3e0b2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class FunctionPrefix @@ -184,7 +185,7 @@ class FunctionPrefix */ protected static function addXlfnPrefix(string $functionString): string { - return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1(', $functionString); + return Preg::replace(self::XLFNREGEXP, '_xlfn.$1(', $functionString); } /** @@ -192,7 +193,7 @@ protected static function addXlfnPrefix(string $functionString): string */ protected static function addXlwsPrefix(string $functionString): string { - return (string) preg_replace(self::XLWSREGEXP, '_xlws.$1(', $functionString); + return Preg::replace(self::XLWSREGEXP, '_xlws.$1(', $functionString); } /** @@ -200,9 +201,9 @@ protected static function addXlwsPrefix(string $functionString): string */ public static function addFunctionPrefix(string $functionString): string { - $functionString = (string) preg_replace_callback( + $functionString = Preg::replaceCallback( Calculation::CALCULATION_REGEXP_CELLREF_SPILL, - fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')', + fn (array $matches) => 'ANCHORARRAY(' . substr((string) $matches[0], 0, -1) . ')', $functionString ); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 929b4f8c09..22b702b297 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; @@ -1535,18 +1536,17 @@ private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell private function parseRef(string $coordinate, string $ref): string { - if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) !== 1) { + if (!Preg::isMatch('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches)) { return $ref; } if (!isset($matches[3])) { // single cell, not range return $coordinate; } $minRow = (int) $matches[2]; - // https://github.com/phpstan/phpstan/issues/11602 - $maxRow = (int) $matches[5]; // @phpstan-ignore-line + $maxRow = (int) $matches[5]; $rows = $maxRow - $minRow + 1; $minCol = Coordinate::columnIndexFromString($matches[1]); - $maxCol = Coordinate::columnIndexFromString($matches[4]); // @phpstan-ignore-line + $maxCol = Coordinate::columnIndexFromString($matches[4]); $cols = $maxCol - $minCol + 1; $firstCellArray = Coordinate::indexesFromString($coordinate); $lastRow = $firstCellArray[1] + $rows - 1; diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php index ac814b8ce3..9d10cdf10d 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; use PhpOffice\PhpSpreadsheet\Writer\Xls\Parser; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ParserTest extends TestCase @@ -54,4 +55,48 @@ public function testGoodParse(): void $parser = new Parser($this->spreadsheet); self::assertSame('1e01001e02001e0300', bin2hex($parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => 3]))); } + + #[DataProvider('cellSheetnameQuotedProvider')] + public function testCellSheetnameQuoted(bool $expected, string $address): void + { + self::assertSame($expected, Parser::matchCellSheetnameQuoted($address)); + } + + public static function cellSheetnameQuotedProvider(): array + { + return [ + [true, '\'TS GK Mustermann Hans 2\'!$N$1'], + [true, '\'TS GK Mustermann Hans 2\'!N15'], + [true, '\'TS GK Mus\'\'termann Hans 2\'!N15'], + [false, '\'TS GK Mus\'termann Hans 2\'!N15'], + [false, '\'TS GK Mustermann Hans 2\'!N15:P16'], + [false, '\'TS GK Mustermann Hans 2\'!$N$15:$P$16'], + [false, 'sheet1!N15'], + [false, 'sheet1!N15:P16'], + [false, 'N15'], + [false, 'N15:P16'], + ]; + } + + #[DataProvider('rangeSheetnameQuotedProvider')] + public function testRangeSheetnameQuoted(bool $expected, string $address): void + { + self::assertSame($expected, Parser::matchRangeSheetnameQuoted($address)); + } + + public static function rangeSheetnameQuotedProvider(): array + { + return [ + [false, '\'TS GK Mustermann Hans 2\'!$N$1'], + [false, '\'TS GK Mustermann Hans 2\'!N15'], + [false, '\'TS GK Mus\'\'termann Hans 2\'!N15'], + [false, '\'TS GK Mus\'termann Hans 2\'!N15'], + [true, '\'TS GK Mustermann Hans 2\'!N15:P16'], + [true, '\'TS GK Mustermann Hans 2\'!$N$15:$P$16'], + [false, 'sheet1!N15'], + [false, 'sheet1!N15:P16'], + [false, 'N15'], + [false, 'N15:P16'], + ]; + } }