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 @@
+
+
+ |
+
+ ';
+ $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 = '
+
+ |
+
+
';
+ $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 = '
+
+ |
+
+
';
+ $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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ 3
+ 6
+
+
+ False
+ False
+
+
+
\ No newline at end of file