From 52f30052e1d3668521b8e03c9264ea99b16ece4f Mon Sep 17 00:00:00 2001 From: oleibman Date: Wed, 11 Nov 2020 02:02:04 -0800 Subject: [PATCH 01/38] Make DefinedNames Samples Consistent With Other Samples (#1707) All other Samples write to temporary directory. DefinedNames samples write to main directory, which (a) means they aren't stored with others, and (b) they aren't ignored by git so look like changed files. The tests are also simplified by requiring Header rather than Bootstrap, making use of Helper. --- samples/DefinedNames/AbsoluteNamedRange.php | 17 ++++------------- .../DefinedNames/CrossWorksheetNamedFormula.php | 15 +++------------ samples/DefinedNames/NamedFormulaeAndRanges.php | 17 ++++------------- samples/DefinedNames/RelativeNamedRange.php | 17 ++++------------- samples/DefinedNames/RelativeNamedRange2.php | 17 ++++------------- .../RelativeNamedRangeAsFunction.php | 17 ++++------------- samples/DefinedNames/ScopedNamedRange.php | 17 ++++------------- samples/DefinedNames/ScopedNamedRange2.php | 17 ++++------------- samples/DefinedNames/SimpleNamedFormula.php | 17 ++++------------- samples/DefinedNames/SimpleNamedRange.php | 17 ++++------------- 10 files changed, 39 insertions(+), 129 deletions(-) diff --git a/samples/DefinedNames/AbsoluteNamedRange.php b/samples/DefinedNames/AbsoluteNamedRange.php index 450de66760..30afc00d65 100644 --- a/samples/DefinedNames/AbsoluteNamedRange.php +++ b/samples/DefinedNames/AbsoluteNamedRange.php @@ -1,16 +1,9 @@ setActiveSheetIndex(0); @@ -51,13 +44,11 @@ ->setCellValue("B{$row}", "=SUM(B{$startRow}:B{$endRow})") ->setCellValue("C{$row}", "=SUM(C{$startRow}:C{$endRow})"); -echo sprintf( +$helper->log(sprintf( 'Worked %.2f hours at a rate of %.2f - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $worksheet->getCell('B1')->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'AbsoluteNamedRange.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/CrossWorksheetNamedFormula.php b/samples/DefinedNames/CrossWorksheetNamedFormula.php index b441e0f7da..5ce7651628 100644 --- a/samples/DefinedNames/CrossWorksheetNamedFormula.php +++ b/samples/DefinedNames/CrossWorksheetNamedFormula.php @@ -1,18 +1,11 @@ getCell("B{$row}")->getFormattedValue(); $profitGrowth = $worksheet->getCell("C{$row}")->getFormattedValue(); - echo "Growth for {$month} is {$growth}, with a Profit Growth of {$profitGrowth}", PHP_EOL; + $helper->log("Growth for {$month} is {$growth}, with a Profit Growth of {$profitGrowth}"); } -$outputFileName = 'CrossWorksheetNamedFormula.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/NamedFormulaeAndRanges.php b/samples/DefinedNames/NamedFormulaeAndRanges.php index b3f94efcaf..a5ca80e706 100644 --- a/samples/DefinedNames/NamedFormulaeAndRanges.php +++ b/samples/DefinedNames/NamedFormulaeAndRanges.php @@ -1,17 +1,10 @@ setActiveSheetIndex(0); @@ -62,13 +55,11 @@ ->setCellValue("B{$row}", '=COLUMN_TOTALS') ->setCellValue("C{$row}", '=COLUMN_TOTALS'); -echo sprintf( +$helper->log(sprintf( 'Worked %.2f hours at a rate of %.2f - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $worksheet->getCell('B1')->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'NamedFormulaeAndRanges.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/RelativeNamedRange.php b/samples/DefinedNames/RelativeNamedRange.php index 1f712017af..fac75a4715 100644 --- a/samples/DefinedNames/RelativeNamedRange.php +++ b/samples/DefinedNames/RelativeNamedRange.php @@ -1,16 +1,9 @@ setActiveSheetIndex(0); @@ -54,13 +47,11 @@ ->setCellValue("B{$row}", "=SUM(B{$startRow}:B{$endRow})") ->setCellValue("C{$row}", "=SUM(C{$startRow}:C{$endRow})"); -echo sprintf( +$helper->log(sprintf( 'Worked %.2f hours at a rate of %.2f - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $worksheet->getCell('B1')->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'RelativeNamedRange.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/RelativeNamedRange2.php b/samples/DefinedNames/RelativeNamedRange2.php index b6a6f8d1c5..b3e957fd2e 100644 --- a/samples/DefinedNames/RelativeNamedRange2.php +++ b/samples/DefinedNames/RelativeNamedRange2.php @@ -1,16 +1,9 @@ setActiveSheetIndex(0); @@ -57,13 +50,11 @@ ->setCellValue("B{$row}", '=SUM(COLUMN_DATA_VALUES)') ->setCellValue("C{$row}", '=SUM(COLUMN_DATA_VALUES)'); -echo sprintf( +$helper->log(sprintf( 'Worked %.2f hours at a rate of %.2f - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $worksheet->getCell('B1')->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'RelativeNamedRange2.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/RelativeNamedRangeAsFunction.php b/samples/DefinedNames/RelativeNamedRangeAsFunction.php index 527cd43b58..333d01ab0b 100644 --- a/samples/DefinedNames/RelativeNamedRangeAsFunction.php +++ b/samples/DefinedNames/RelativeNamedRangeAsFunction.php @@ -1,17 +1,10 @@ setActiveSheetIndex(0); @@ -60,13 +53,11 @@ ->setCellValue("B{$row}", '=SUM(COLUMN_DATA_VALUES)') ->setCellValue("C{$row}", '=SUM(COLUMN_DATA_VALUES)'); -echo sprintf( +$helper->log(sprintf( 'Worked %.2f hours at a rate of %.2f - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $worksheet->getCell('B1')->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'RelativeNamedRangeAsFunction.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/ScopedNamedRange.php b/samples/DefinedNames/ScopedNamedRange.php index 5ea7e48ab9..aa71454df0 100644 --- a/samples/DefinedNames/ScopedNamedRange.php +++ b/samples/DefinedNames/ScopedNamedRange.php @@ -1,16 +1,9 @@ setActiveSheetIndex(0); @@ -67,15 +60,13 @@ ->setCellValue("B{$row}", '=SUM(COLUMN_DATA_VALUES)') ->setCellValue("C{$row}", '=SUM(COLUMN_DATA_VALUES)'); -echo sprintf( +$helper->log(sprintf( 'Worked %.2f hours at a rate of %s - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $chargeRateCellValue = $spreadsheet ->getSheetByName($spreadsheet->getNamedRange('CHARGE_RATE')->getWorksheet()->getTitle()) ->getCell($spreadsheet->getNamedRange('CHARGE_RATE')->getCellsInRange()[0])->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'ScopedNamedRange.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/ScopedNamedRange2.php b/samples/DefinedNames/ScopedNamedRange2.php index 8e6104189a..5f0898c91e 100644 --- a/samples/DefinedNames/ScopedNamedRange2.php +++ b/samples/DefinedNames/ScopedNamedRange2.php @@ -1,16 +1,9 @@ setActiveSheetIndex(0); @@ -83,16 +76,14 @@ ->setCellValue('B1', 4.5); foreach ($spreadsheet->getAllSheets() as $worksheet) { - echo sprintf( + $helper->log(sprintf( 'Worked %.2f hours for "%s" at a rate of %.2f - Charge to the client is %.2f', $worksheet->getCell("B{$row}")->getCalculatedValue(), $worksheet->getTitle(), $worksheet->getCell('B1')->getValue(), $worksheet->getCell("C{$row}")->getCalculatedValue() - ), PHP_EOL; + )); } $worksheet = $spreadsheet->setActiveSheetIndex(0); -$outputFileName = 'ScopedNamedRange2.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/SimpleNamedFormula.php b/samples/DefinedNames/SimpleNamedFormula.php index 8e0fc972ce..ea1f802da1 100644 --- a/samples/DefinedNames/SimpleNamedFormula.php +++ b/samples/DefinedNames/SimpleNamedFormula.php @@ -1,17 +1,10 @@ setActiveSheetIndex(0); @@ -39,14 +32,12 @@ ->setCellValue('B4', '=TAX') ->setCellValue('B5', '=PRICE+TAX'); -echo sprintf( +$helper->log(sprintf( 'With a Tax Rate of %.2f and a net price of %.2f, Tax is %.2f and the gross price is %.2f', $worksheet->getCell('B1')->getCalculatedValue(), $worksheet->getCell('B3')->getValue(), $worksheet->getCell('B4')->getCalculatedValue(), $worksheet->getCell('B5')->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'SimpleNamedFormula.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/DefinedNames/SimpleNamedRange.php b/samples/DefinedNames/SimpleNamedRange.php index ee05c68f46..7a7cdc94a1 100644 --- a/samples/DefinedNames/SimpleNamedRange.php +++ b/samples/DefinedNames/SimpleNamedRange.php @@ -1,16 +1,9 @@ setActiveSheetIndex(0); @@ -33,14 +26,12 @@ ->setCellValue('B4', '=PRICE*TAX_RATE') ->setCellValue('B5', '=PRICE*(1+TAX_RATE)'); -echo sprintf( +$helper->log(sprintf( 'With a Tax Rate of %.2f and a net price of %.2f, Tax is %.2f and the gross price is %.2f', $worksheet->getCell('B1')->getCalculatedValue(), $worksheet->getCell('B3')->getValue(), $worksheet->getCell('B4')->getCalculatedValue(), $worksheet->getCell('B5')->getCalculatedValue() -), PHP_EOL; +)); -$outputFileName = 'SimpleNamedRange.xlsx'; -$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); -$writer->save($outputFileName); +$helper->write($spreadsheet, __FILE__, ['Xlsx']); From e05442b0fff591bea882a58b63b61b3df3227fb7 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Thu, 19 Nov 2020 11:59:57 +0100 Subject: [PATCH 02/38] Resolve XSS Vulnerability in the HTML Writer (#1719) Resolve XSS Vulnerability in the HTML Writer --- CHANGELOG.md | 4 + composer.json | 11 +- composer.lock | 858 ++++++++++++------ src/PhpSpreadsheet/Writer/Html.php | 11 +- .../Writer/Html/XssVulnerabilityTest.php | 49 + 5 files changed, 631 insertions(+), 302 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f149df2a..2b366f114a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). - Fix for notice during accessing "cached magnification factor" offset [#1354](https://github.com/PHPOffice/PhpSpreadsheet/pull/1354) +### Security Fix (CVE-2020-7776) + +- Prevent XSS through cell comments in the HTML Writer. + ## 1.15.0 - 2020-10-11 ### Added diff --git a/composer.json b/composer.json index 0fcb2556dd..7e3b840e46 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ ] }, "require": { - "php": "^7.2|^8.0", + "php": "^7.2||^8.0", "ext-ctype": "*", "ext-dom": "*", "ext-gd": "*", @@ -54,11 +54,12 @@ "ext-zip": "*", "ext-zlib": "*", "maennchen/zipstream-php": "^2.1", - "markbaker/complex": "^1.5|^2.0", - "markbaker/matrix": "^1.2|^2.0", + "markbaker/complex": "^1.5||^2.0", + "markbaker/matrix": "^1.2||^2.0", "psr/simple-cache": "^1.0", "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" + "psr/http-factory": "^1.0", + "voku/anti-xss": "^4.1" }, "require-dev": { "dompdf/dompdf": "^0.8.5", @@ -66,7 +67,7 @@ "jpgraph/jpgraph": "^4.0", "mpdf/mpdf": "^8.0", "phpcompatibility/php-compatibility": "^9.3", - "phpunit/phpunit": "^8.5|^9.3", + "phpunit/phpunit": "^8.5||^9.3", "squizlabs/php_codesniffer": "^3.5", "tecnickcom/tcpdf": "^6.3" }, diff --git a/composer.lock b/composer.lock index 9be3b1b9ed..5dd7483349 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "955c4a84deddf2db043a5c2faabbb66f", + "content-hash": "209605c0b9329968170279f40db65d22", "packages": [ { "name": "maennchen/zipstream-php", @@ -65,12 +65,6 @@ "stream", "zip" ], - "funding": [ - { - "url": "https://opencollective.com/zipstream", - "type": "open_collective" - } - ], "time": "2020-05-30T13:11:16+00:00" }, { @@ -483,6 +477,242 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/c536646fdb4f29104dd26effc2fdcb9a5b085024", + "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.18.1", @@ -559,6 +789,323 @@ } ], "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "639447d008615574653fb3bc60d1986d7172eaae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", + "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "voku/anti-xss", + "version": "4.1.30", + "source": { + "type": "git", + "url": "https://github.com/voku/anti-xss.git", + "reference": "ff6e54f4a98ad1cd28f8b4a0f3c3f92f3c421f0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/anti-xss/zipball/ff6e54f4a98ad1cd28f8b4a0f3c3f92f3c421f0a", + "reference": "ff6e54f4a98ad1cd28f8b4a0f3c3f92f3c421f0a", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "voku/portable-utf8": "~5.4.50" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "voku\\helper\\": "src/voku/helper/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "EllisLab Dev Team", + "homepage": "http://ellislab.com/" + }, + { + "name": "Lars Moelleken", + "email": "lars@moelleken.org", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "anti xss-library", + "homepage": "https://github.com/voku/anti-xss", + "keywords": [ + "anti-xss", + "clean", + "security", + "xss" + ], + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/anti-xss", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/anti-xss", + "type": "tidelift" + } + ], + "time": "2020-11-12T00:30:57+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "80953678b19901e5165c56752d087fc11526017c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/80953678b19901e5165c56752d087fc11526017c", + "reference": "80953678b19901e5165c56752d087fc11526017c", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2020-11-12T00:07:28+00:00" + }, + { + "name": "voku/portable-utf8", + "version": "5.4.50", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-utf8.git", + "reference": "f14ed68ea9ced6639e71ca989c6d907892115ba0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-utf8/zipball/f14ed68ea9ced6639e71ca989c6d907892115ba0", + "reference": "f14ed68ea9ced6639e71ca989c6d907892115ba0", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "symfony/polyfill-iconv": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php72": "~1.0", + "voku/portable-ascii": "~1.5.6" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-ctype": "Use Ctype for e.g. hexadecimal digit detection", + "ext-fileinfo": "Use Fileinfo for better binary file detection", + "ext-iconv": "Use iconv for best performance", + "ext-intl": "Use Intl for best performance", + "ext-json": "Use JSON for string detection", + "ext-mbstring": "Use Mbstring for best performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "(Apache-2.0 or GPL-2.0)" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Hamid Sarfraz", + "homepage": "http://pageconfig.com/" + }, + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable UTF-8 library - performance optimized (unicode) string functions for php.", + "homepage": "https://github.com/voku/portable-utf8", + "keywords": [ + "UTF", + "clean", + "php", + "unicode", + "utf-8", + "utf8" + ], + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-utf8", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-utf8", + "type": "tidelift" + } + ], + "time": "2020-11-12T00:17:47+00:00" } ], "packages-dev": [ @@ -819,20 +1366,6 @@ "constructor", "instantiate" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -883,32 +1416,18 @@ }, { "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" + "email": "schmittjoh@gmail.com" } ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -1068,12 +1587,6 @@ } ], "description": "A tool to automatically fix PHP code style", - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], "time": "2020-06-27T23:57:46+00:00" }, { @@ -2038,7 +2551,6 @@ "keywords": [ "tokenizer" ], - "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { @@ -2122,16 +2634,6 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-06-22T07:06:58+00:00" }, { @@ -3648,165 +4150,6 @@ ], "time": "2020-07-14T12:35:20+00:00" }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", - "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, { "name": "symfony/polyfill-php70", "version": "v1.18.1", @@ -3884,79 +4227,6 @@ ], "time": "2020-07-14T12:35:20+00:00" }, - { - "name": "symfony/polyfill-php72", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, { "name": "symfony/polyfill-php73", "version": "v1.18.1", diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 5c21b3a94e..31cc05afe8 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -23,6 +23,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use voku\helper\AntiXSS; class Html extends BaseWriter { @@ -1788,9 +1789,13 @@ private function writeComment(Worksheet $pSheet, $coordinate) { $result = ''; if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) { - $result .= ''; - $result .= '
' . nl2br($pSheet->getComment($coordinate)->getText()->getPlainText()) . '
'; - $result .= PHP_EOL; + $sanitizer = new AntiXSS(); + $sanitizedString = $sanitizer->xss_clean($pSheet->getComment($coordinate)->getText()->getPlainText()); + if (!$sanitizer->isXssFound()) { + $result .= ''; + $result .= '
' . nl2br($sanitizedString) . '
'; + $result .= PHP_EOL; + } } return $result; diff --git a/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php b/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php new file mode 100644 index 0000000000..30d7af63f4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php @@ -0,0 +1,49 @@ + [''], + 'javascript tag' => ['javascript:alert(1)'], + 'with unicode' => ['java\u0003script:alert(1)'], + ]; + } + + /** + * @dataProvider providerXssRichText + * + * @param string $xssTextString + */ + public function testXssInComment($xssTextString): void + { + $spreadsheet = new Spreadsheet(); + + $richText = new RichText(); + $richText->createText($xssTextString); + + $spreadsheet->getActiveSheet()->getCell('A1')->setValue('XSS Test'); + + $spreadsheet->getActiveSheet() + ->getComment('A1') + ->setText($richText); + + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + + $writer = IOFactory::createWriter($spreadsheet, 'Html'); + $writer->save($filename); + + $verify = file_get_contents($filename); + // Ensure that executable js has been stripped from the comments + self::assertStringNotContainsString($xssTextString, $verify); + } +} From 5596d6d47abdbda8a7c22b91449ddba44e3ef1d0 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Thu, 26 Nov 2020 11:10:52 +0900 Subject: [PATCH 03/38] Drop Travis --- .gitattributes | 1 - .github/workflows/main.yml | 2 +- .travis.yml | 34 ------------------- README.md | 2 +- .../PhpSpreadsheetTests/Helper/SampleTest.php | 2 +- .../Reader/Xml/XmlLoadTest.php | 7 +--- 6 files changed, 4 insertions(+), 44 deletions(-) delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index 0186deae8c..19c90f409b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,7 +5,6 @@ /.php_cs.dist export-ignore /.sami.php export-ignore /.scrutinizer.yml export-ignore -/.travis.yml export-ignore /CHANGELOG.PHPExcel.md export-ignore /bin export-ignore /composer.lock export-ignore diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 88168fa007..febc244bba 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Build +name: main on: [ push, pull_request ] jobs: test: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6eb592905b..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: php -dist: bionic - -php: - - 7.2 - - 7.3 - - 7.4 - - nightly - -cache: - directories: - - vendor - - $HOME/.composer/cache - -before_script: - # Deactivate xdebug - - if [[ $TRAVIS_PHP_VERSION != nightly ]]; then phpenv config-rm xdebug.ini; fi - - if [[ $TRAVIS_PHP_VERSION == nightly ]]; then rm composer.lock; fi - - composer install --ignore-platform-reqs - -script: - - ./vendor/bin/phpunit --color=always --coverage-text - -allow_failures: - - php: nightly - -jobs: - include: - - - stage: Code style - php: 7.4 - script: - - ./vendor/bin/php-cs-fixer fix --diff --verbose --dry-run - - ./vendor/bin/phpcs diff --git a/README.md b/README.md index 0cef8326ef..2a94e0d3d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PhpSpreadsheet -[![Build Status](https://travis-ci.org/PHPOffice/PhpSpreadsheet.svg?branch=master)](https://travis-ci.org/PHPOffice/PhpSpreadsheet) +[![Build Status](https://github.com/PHPOffice/PhpSpreadsheet/workflows/main/badge.svg)](https://github.com/PHPOffice/PhpSpreadsheet/actions) [![Code Quality](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master) [![Total Downloads](https://img.shields.io/packagist/dt/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index 369b205e08..8fd6bb47cb 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -43,7 +43,7 @@ public function providerSample() } // Unfortunately some tests are too long be ran with code-coverage - // analysis on Travis, so we need to exclude them + // analysis on GitHub Actions, so we need to exclude them global $argv; if (in_array('--coverage-clover', $argv)) { $tooLongToBeCovered = [ diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php index 03f977f5f4..969571f2a5 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php @@ -82,14 +82,9 @@ public function testLoadSelectedSheets(): void public function testLoadUnusableSample(): void { // Sample spreadsheet is not readable by Excel. - // But PhpSpreadsheet can load it except for coverage test. - //global $argv; - //if (in_array('--coverage-clover', $argv)) { - // self::markTestSkipped('Mysterious Travis coverage failure IOFactoryTest'); - //} + // But PhpSpreadsheet can load it. $filename = __DIR__ . '/../../../..' - //. '/samples/templates/Excel2003XMLTest.xml'; . '/samples/templates/excel2003.short.bad.xml'; $reader = new Xml(); $spreadsheet = $reader->load($filename); From d75bba469a0c3d0ecd65a18e090277aa4e649a7b Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Thu, 26 Nov 2020 12:44:07 +0900 Subject: [PATCH 04/38] Automatic GitHub releases from git tags --- .github/workflows/main.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index febc244bba..f4e13a6516 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -148,3 +148,25 @@ jobs: ./vendor/bin/phpunit --coverage-clover coverage-clover.xml curl -LO https://scrutinizer-ci.com/ocular.phar php ocular.phar code-coverage:upload --format=php-clover coverage-clover.xml + + release: + runs-on: ubuntu-latest + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} # Otherwise our annotated tag is not fetched and we cannot get correct version + + # Create release + - name: Get release info + id: release-info + run: | + echo "::set-output name=subject::$(git tag --format '%(contents:subject)' --points-at)" + git tag --format '%(contents:body)' --points-at > release-body.txt + - uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: ${{ steps.release-info.outputs.subject }} + body_path: release-body.txt From e4be949dbc0c76991259767ca666b891efdc8a80 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Fri, 27 Nov 2020 06:50:01 -0800 Subject: [PATCH 05/38] Improve Coverage in src/PhpSpreadsheet There are no changes to code. Additional tests are added, so that the following 6 items now have 100% test coverage: - Comment - DefinedName - DocumentGenerator - IOFactory - NamedFormula - NamedRange --- samples/Reader/sampleData/example1xls | Bin 0 -> 22528 bytes .../DefinedNameFormulaTest.php | 21 +++++ tests/PhpSpreadsheetTests/DefinedNameTest.php | 82 ++++++++++++++++++ .../DocumentGeneratorTest.php | 10 +++ .../Functional/CommentsTest.php | 15 +++- tests/PhpSpreadsheetTests/IOFactoryTest.php | 41 +++++++++ 6 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 samples/Reader/sampleData/example1xls diff --git a/samples/Reader/sampleData/example1xls b/samples/Reader/sampleData/example1xls new file mode 100644 index 0000000000000000000000000000000000000000..bd9bb110b9a41b0aab816ba9e20682ee5bb43aea GIT binary patch literal 22528 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 literal 0 HcmV?d00001 diff --git a/tests/PhpSpreadsheetTests/DefinedNameFormulaTest.php b/tests/PhpSpreadsheetTests/DefinedNameFormulaTest.php index 503049910c..1484309159 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameFormulaTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameFormulaTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\DefinedName; +use PhpOffice\PhpSpreadsheet\NamedFormula; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -164,4 +165,24 @@ public function providerRangeOrFormula(): array 'utf-8 named ranges in a formula' => ['Здравствуй+мир', true], ]; } + + public function testEmptyNamedFormula(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadSheet = new Spreadsheet(); + $workSheet1 = $spreadSheet->getActiveSheet(); + new NamedFormula('namedformula', $workSheet1); + } + + public function testChangeFormula(): void + { + $spreadSheet = new Spreadsheet(); + $workSheet1 = $spreadSheet->getActiveSheet(); + $namedFormula = new NamedFormula('namedformula', $workSheet1, '=1'); + self::assertEquals('=1', $namedFormula->getFormula()); + $namedFormula->setFormula('=2'); + self::assertEquals('=2', $namedFormula->getFormula()); + $namedFormula->setFormula(''); + self::assertEquals('=2', $namedFormula->getFormula()); + } } diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 3f5b75a017..c199e7f2c1 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\DefinedName; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -121,4 +122,85 @@ public function testRemoveScopedDefinedNameWhenDuplicateNames(): void $this->spreadsheet->getDefinedName('foo')->getValue() ); } + + public function testDefinedNameNoWorksheetNoScope(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + new NamedRange('xyz'); + } + + public function testSetAndGetRange(): void + { + $this->spreadsheet->addDefinedName( + DefinedName::createInstance('xyz', $this->spreadsheet->getActiveSheet(), 'A1') + ); + + $namedRange = $this->spreadsheet->getDefinedName('XYZ'); + self::assertInstanceOf(NamedRange::class, $namedRange); + if ($namedRange instanceof NamedRange) { + self::assertEquals('A1', $namedRange->getRange()); + self::assertEquals('A1', $namedRange->getValue()); + $namedRange->setRange('A2'); + self::assertEquals('A2', $namedRange->getValue()); + } + } + + public function testChangeWorksheet(): void + { + $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); + $sheet1->getCell('A1')->setValue(1); + $sheet2->getCell('A1')->setValue(2); + $namedRange = new NamedRange('xyz', $sheet2, '$A$1'); + $namedRange->setWorksheet($sheet1); + $this->spreadsheet->addNamedRange($namedRange); + $sheet1->getCell('B2')->setValue('=XYZ'); + self::assertEquals(1, $sheet1->getCell('B2')->getCalculatedValue()); + $sheet2->getCell('B2')->setValue('=XYZ'); + self::assertEquals(1, $sheet2->getCell('B2')->getCalculatedValue()); + } + + public function testLocalOnly(): void + { + $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); + $sheet1->getCell('A1')->setValue(1); + $sheet2->getCell('A1')->setValue(2); + $namedRange = new NamedRange('abc', $sheet2, '$A$1'); + $namedRange->setWorksheet($sheet1)->setLocalOnly(true); + $this->spreadsheet->addNamedRange($namedRange); + $sheet1->getCell('C2')->setValue('=ABC'); + self::assertEquals(1, $sheet1->getCell('C2')->getCalculatedValue()); + $sheet2->getCell('C2')->setValue('=ABC'); + self::assertEquals('#NAME?', $sheet2->getCell('C2')->getCalculatedValue()); + } + + public function testScope(): void + { + $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); + $sheet1->getCell('A1')->setValue(1); + $sheet2->getCell('A1')->setValue(2); + $namedRange = new NamedRange('abc', $sheet2, '$A$1'); + $namedRange->setScope($sheet1); + $this->spreadsheet->addNamedRange($namedRange); + $sheet1->getCell('C2')->setValue('=ABC'); + self::assertEquals(2, $sheet1->getCell('C2')->getCalculatedValue()); + $sheet2->getCell('C2')->setValue('=ABC'); + self::assertEquals('#NAME?', $sheet2->getCell('C2')->getCalculatedValue()); + } + + public function testClone(): void + { + $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); + $sheet1->getCell('A1')->setValue(1); + $sheet2->getCell('A1')->setValue(2); + $namedRange = new NamedRange('abc', $sheet2, '$A$1'); + $namedRangeClone = clone $namedRange; + $ss1 = $namedRange->getWorksheet(); + $ss2 = $namedRangeClone->getWorksheet(); + self::assertNotSame($ss1, $ss2); + self::assertEquals($ss1->getTitle(), $ss2->getTitle()); + } } diff --git a/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php b/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php index 8a34f1c0bb..45000f8e1f 100644 --- a/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Logical; use PhpOffice\PhpSpreadsheet\DocumentGenerator; use PHPUnit\Framework\TestCase; +use UnexpectedValueException; class DocumentGeneratorTest extends TestCase { @@ -137,4 +138,13 @@ public function providerGenerateFunctionListByCategory(): array ], ]; } + + public function testGenerateFunctionBadArray(): void + { + $this->expectException(UnexpectedValueException::class); + $phpSpreadsheetFunctions = [ + 'ABS' => ['category' => Cat::CATEGORY_MATH_AND_TRIG, 'functionCall' => 1], + ]; + DocumentGenerator::generateFunctionListByName($phpSpreadsheetFunctions); + } } diff --git a/tests/PhpSpreadsheetTests/Functional/CommentsTest.php b/tests/PhpSpreadsheetTests/Functional/CommentsTest.php index 6f1c534026..5ba4e7c833 100644 --- a/tests/PhpSpreadsheetTests/Functional/CommentsTest.php +++ b/tests/PhpSpreadsheetTests/Functional/CommentsTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Functional; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; class CommentsTest extends AbstractFunctional { @@ -35,10 +36,22 @@ public function testComments($format): void $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); - $commentsLoaded = $reloadedSpreadsheet->getSheet(0)->getComments(); + $sheet = $reloadedSpreadsheet->getSheet(0); + $commentsLoaded = $sheet->getComments(); self::assertCount(1, $commentsLoaded); $commentCoordinate = key($commentsLoaded); self::assertSame('E10', $commentCoordinate); + $comment = $commentsLoaded[$commentCoordinate]; + self::assertEquals('Comment to test', (string) $comment); + $commentClone = clone $comment; + self::assertEquals($comment, $commentClone); + self::assertNotSame($comment, $commentClone); + if ($format === 'Xlsx') { + self::assertEquals('feb0c24b880a8130262dadf801f85e94', $comment->getHashCode()); + self::assertEquals(Alignment::HORIZONTAL_GENERAL, $comment->getAlignment()); + $comment->setAlignment(Alignment::HORIZONTAL_RIGHT); + self::assertEquals(Alignment::HORIZONTAL_RIGHT, $comment->getAlignment()); + } } } diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index f9512a3400..9b07ad3314 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -161,4 +161,45 @@ public function testRegisterInvalidReader(): void IOFactory::registerReader('foo', 'bar'); } + + public function testCreateInvalidWriter(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Writer\Exception::class); + $spreadsheet = new Spreadsheet(); + IOFactory::createWriter($spreadsheet, 'bad'); + } + + public function testCreateInvalidReader(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class); + IOFactory::createReader('bad'); + } + + public function testCreateReaderUnknownExtension(): void + { + $filename = 'samples/Reader/sampleData/example1.tsv'; + $reader = IOFactory::createReaderForFile($filename); + self::assertEquals('PhpOffice\\PhpSpreadsheet\\Reader\\Csv', get_class($reader)); + } + + public function testCreateReaderCsvExtension(): void + { + $filename = 'samples/Reader/sampleData/example1.csv'; + $reader = IOFactory::createReaderForFile($filename); + self::assertEquals('PhpOffice\\PhpSpreadsheet\\Reader\\Csv', get_class($reader)); + } + + public function testCreateReaderNoExtension(): void + { + $filename = 'samples/Reader/sampleData/example1xls'; + $reader = IOFactory::createReaderForFile($filename); + self::assertEquals('PhpOffice\\PhpSpreadsheet\\Reader\\Xls', get_class($reader)); + } + + public function testCreateReaderNotSpreadsheet(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class); + $filename = __FILE__; + $reader = IOFactory::createReaderForFile($filename); + } } From 39025e8e426b2e7e64825381dd7c8f0feff380c0 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Fri, 27 Nov 2020 07:16:23 -0800 Subject: [PATCH 06/38] Changes for Scrutinizer Two changes to fix minor problems reported by Scrutinizer. --- tests/PhpSpreadsheetTests/DefinedNameTest.php | 10 ++++------ tests/PhpSpreadsheetTests/IOFactoryTest.php | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index c199e7f2c1..8a41177593 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -137,12 +137,10 @@ public function testSetAndGetRange(): void $namedRange = $this->spreadsheet->getDefinedName('XYZ'); self::assertInstanceOf(NamedRange::class, $namedRange); - if ($namedRange instanceof NamedRange) { - self::assertEquals('A1', $namedRange->getRange()); - self::assertEquals('A1', $namedRange->getValue()); - $namedRange->setRange('A2'); - self::assertEquals('A2', $namedRange->getValue()); - } + self::assertEquals('A1', $namedRange->getRange()); + self::assertEquals('A1', $namedRange->getValue()); + $namedRange->setRange('A2'); + self::assertEquals('A2', $namedRange->getValue()); } public function testChangeWorksheet(): void diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index 9b07ad3314..886fcb3613 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -200,6 +200,6 @@ public function testCreateReaderNotSpreadsheet(): void { $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class); $filename = __FILE__; - $reader = IOFactory::createReaderForFile($filename); + IOFactory::createReaderForFile($filename); } } From 6e71f80ecb65e43f4ab4ae5e71498eaeed90ee90 Mon Sep 17 00:00:00 2001 From: Ryan McAllen Date: Mon, 7 Dec 2020 10:17:56 -0500 Subject: [PATCH 07/38] Spelling: Tou -> You --- src/PhpSpreadsheet/NamedFormula.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/NamedFormula.php b/src/PhpSpreadsheet/NamedFormula.php index ffb1c9b480..eeddbbcb99 100644 --- a/src/PhpSpreadsheet/NamedFormula.php +++ b/src/PhpSpreadsheet/NamedFormula.php @@ -18,7 +18,7 @@ public function __construct( ) { // Validate data if (empty($formula)) { - throw new Exception('Tou must specify a Formula value for a Named Formula'); + throw new Exception('You must specify a Formula value for a Named Formula'); } parent::__construct($name, $worksheet, $formula, $localOnly, $scope); } From 95cb746947e1f0949da660f13051206bd62e7a95 Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 10 Dec 2020 09:01:08 -0800 Subject: [PATCH 08/38] Fix for 1735 (Incorrect activeSheetIndex after RemoveSheetByIndex) (#1743) This is a fix for issue #1735. It adds tests for this situation, and similar situations involving adding new sheets and accessing existing ones. Coverage for Spreadsheet.php increases from 69% to 75% as a result. --- src/PhpSpreadsheet/Spreadsheet.php | 2 +- tests/PhpSpreadsheetTests/SpreadsheetTest.php | 143 +++++++++++++++++- 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 19c1152642..c8e8f72cfa 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -665,7 +665,7 @@ public function removeSheetByIndex($pIndex): void // Adjust active sheet index if necessary if ( ($this->activeSheetIndex >= $pIndex) && - ($pIndex > count($this->workSheetCollection) - 1) + ($this->activeSheetIndex > 0 || $numSheets <= 1) ) { --$this->activeSheetIndex; } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index 05fbe1b549..129bea7c03 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -51,6 +51,147 @@ public function dataProviderForSheetNames() */ public function testGetSheetByName($index, $sheetName): void { - self::assertEquals($this->object->getSheet($index), $this->object->getSheetByName($sheetName)); + self::assertSame($this->object->getSheet($index), $this->object->getSheetByName($sheetName)); + } + + public function testAddSheetDuplicateTitle(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet2'); + $this->object->addSheet($sheet); + } + + public function testAddSheetNoAdjustActive(): void + { + $this->object->setActiveSheetIndex(2); + self::assertEquals(2, $this->object->getActiveSheetIndex()); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet4'); + $this->object->addSheet($sheet); + self::assertEquals(2, $this->object->getActiveSheetIndex()); + } + + public function testAddSheetAdjustActive(): void + { + $this->object->setActiveSheetIndex(2); + self::assertEquals(2, $this->object->getActiveSheetIndex()); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet0'); + $this->object->addSheet($sheet, 0); + self::assertEquals(3, $this->object->getActiveSheetIndex()); + } + + public function testRemoveSheetIndexTooHigh(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->object->removeSheetByIndex(4); + } + + public function testRemoveSheetNoAdjustActive(): void + { + $this->object->setActiveSheetIndex(1); + self::assertEquals(1, $this->object->getActiveSheetIndex()); + $this->object->removeSheetByIndex(2); + self::assertEquals(1, $this->object->getActiveSheetIndex()); + } + + public function testRemoveSheetAdjustActive(): void + { + $this->object->setActiveSheetIndex(2); + self::assertEquals(2, $this->object->getActiveSheetIndex()); + $this->object->removeSheetByIndex(1); + self::assertEquals(1, $this->object->getActiveSheetIndex()); + } + + public function testGetSheetIndexTooHigh(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->object->getSheet(4); + } + + public function testGetIndexNonExistent(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet4'); + $this->object->getIndex($sheet); + } + + public function testSetIndexByName(): void + { + $this->object->setIndexByName('someSheet1', 1); + self::assertEquals('someSheet2', $this->object->getSheet(0)->getTitle()); + self::assertEquals('someSheet1', $this->object->getSheet(1)->getTitle()); + self::assertEquals('someSheet 3', $this->object->getSheet(2)->getTitle()); + } + + public function testRemoveAllSheets(): void + { + $this->object->setActiveSheetIndex(2); + self::assertEquals(2, $this->object->getActiveSheetIndex()); + $this->object->removeSheetByIndex(0); + self::assertEquals(1, $this->object->getActiveSheetIndex()); + $this->object->removeSheetByIndex(0); + self::assertEquals(0, $this->object->getActiveSheetIndex()); + $this->object->removeSheetByIndex(0); + self::assertEquals(-1, $this->object->getActiveSheetIndex()); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet4'); + $this->object->addSheet($sheet); + self::assertEquals(0, $this->object->getActiveSheetIndex()); + } + + public function testBug1735(): void + { + $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet->createSheet()->setTitle('addedsheet'); + $spreadsheet->setActiveSheetIndex(1); + $spreadsheet->removeSheetByIndex(0); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('addedsheet', $sheet->getTitle()); + } + + public function testSetActiveSheetIndexTooHigh(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->object->setActiveSheetIndex(4); + } + + public function testSetActiveSheetNoSuchName(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->object->setActiveSheetIndexByName('unknown'); + } + + public function testAddExternal(): void + { + $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $sheet = $spreadsheet->createSheet()->setTitle('someSheet19'); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); + $sheet->getCell('B1')->getStyle()->getFont()->setSuperscript(true); + $sheet->getCell('C1')->getStyle()->getFont()->setSubscript(true); + self::assertCount(4, $spreadsheet->getCellXfCollection()); + self::assertEquals(1, $sheet->getCell('A1')->getXfIndex()); + $this->object->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); + self::assertCount(2, $this->object->getCellXfCollection()); + $sheet3 = $this->object->addExternalSheet($sheet); + self::assertCount(6, $this->object->getCellXfCollection()); + self::assertEquals('someSheet19', $sheet3->getTitle()); + self::assertEquals(1, $sheet3->getCell('A1')->getValue()); + self::assertTrue($sheet3->getCell('A1')->getStyle()->getFont()->getBold()); + // Prove Xf index changed although style is same. + self::assertEquals(3, $sheet3->getCell('A1')->getXfIndex()); + } + + public function testAddExternalDuplicateName(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $sheet = $spreadsheet->createSheet()->setTitle('someSheet1'); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); + $this->object->addExternalSheet($sheet); } } From 37ec4fafd23f6cd29d374d4392bb647ab79ec5dc Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 10 Dec 2020 18:05:44 +0100 Subject: [PATCH 09/38] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b366f114a..3ba28c262b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) - Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). - Fix for notice during accessing "cached magnification factor" offset [#1354](https://github.com/PHPOffice/PhpSpreadsheet/pull/1354) From 7b8f0236110837ed129e5e38ab031ec8ba9b50ac Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 10 Dec 2020 09:08:10 -0800 Subject: [PATCH 10/38] Fix for 3 Issues Involving ReadXlsx and NamedRange (#1742) * Fix for 3 Issues Involving ReadXlsx and NamedRange Issues #1686 and #1723, which provide sample spreadsheets, are probably solved by this ticket. Issue #1730 is also probably solved, but I have no way to verify. There are two problems with how PhpSpreadsheet is handling things now. Although the first problem is much less severe, and isn't really a factor in the issues named above, it is helpful to get it out of the way first. If you define a named range in Excel, and then delete the sheet where the range exists, Excel saves the range as #REF!. If there is a cell which references the range, it will similarly have the value #REF! when you open the Excel file. Currently, PhpSpreadsheet discards the #REF! definition, so a cell which references the range will appear as #NAME? rather than #REF!. This PR changes the behavior so that PhpSpreadsheet retains the #REF! definition, and cells which reference it will appear as #REF!. The second problem is the more severe, and is, I believe, responsible for the 3 issues identified above. If you define a named range and the sheet on which the range is defined does not exist at the time, Excel will save the range as something like: '[1]Unknown Sheet'!$A$1 If a cell references such a range, Excel will again display #REF!. PhpSpreadsheet currently throws an Exception when it encounters such a definition while reading the file. This PR changes the behavior so that PhpSpreadsheet saves the definition as #REF!, and cells which reference it will behave similarly. For the record, I will note that Excel does not magically recalculate when a missing sheet is subsequently added, despite the fact that the reference might now become resolvable. PhpSpreadsheet behaves likewise. * Remove Dead Code in Test Identified it after push but before merge. --- src/PhpSpreadsheet/Reader/Xlsx.php | 7 ++++-- .../Reader/Xlsx/NamedRangeTest.php | 22 ++++++++++++++++++ tests/data/Reader/XLSX/bug1686b.xlsx | Bin 0 -> 11740 bytes 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/NamedRangeTest.php create mode 100644 tests/data/Reader/XLSX/bug1686b.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 3a75edf67d..5dc48ff830 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1280,7 +1280,7 @@ public function load($pFilename) } // Valid range? - if (stripos((string) $definedName, '#REF!') !== false || $extractedRange == '') { + if ($extractedRange == '') { continue; } @@ -1350,7 +1350,7 @@ public function load($pFilename) $extractedRange = (string) $definedName; // Valid range? - if (stripos((string) $definedName, '#REF!') !== false || $extractedRange == '') { + if ($extractedRange == '') { continue; } @@ -1398,6 +1398,9 @@ public function load($pFilename) $locatedSheet = $excel->getSheetByName($extractedSheetName); } + if ($locatedSheet === null && !DefinedName::testIfFormula($definedRange)) { + $definedRange = '#REF!'; + } $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $definedRange, false)); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamedRangeTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamedRangeTest.php new file mode 100644 index 0000000000..e4090d9832 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamedRangeTest.php @@ -0,0 +1,22 @@ +load($xlsxFile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals(2.1, $sheet->getCell('A1')->getCalculatedValue()); + self::assertEquals('#REF!', $sheet->getCell('A2')->getCalculatedValue()); + self::assertEquals('#REF!', $sheet->getCell('A3')->getCalculatedValue()); + self::assertEquals('#NAME?', $sheet->getCell('A4')->getCalculatedValue()); + self::assertEquals('#REF!', $sheet->getCell('A5')->getCalculatedValue()); + } +} diff --git a/tests/data/Reader/XLSX/bug1686b.xlsx b/tests/data/Reader/XLSX/bug1686b.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d496c5c76e8235f805e988117eaa75e1a39782f0 GIT binary patch literal 11740 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 Date: Thu, 10 Dec 2020 18:13:12 +0100 Subject: [PATCH 11/38] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba28c262b..c818cb9358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Resolve issues with defined names where worksheet doesn't exist (#1686)[https://github.com/PHPOffice/PhpSpreadsheet/issues/1686] and [#1723](https://github.com/PHPOffice/PhpSpreadsheet/issues/1723) - [PR #1742](https://github.com/PHPOffice/PhpSpreadsheet/pull/1742) - Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) - Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). - Fix for notice during accessing "cached magnification factor" offset [#1354](https://github.com/PHPOffice/PhpSpreadsheet/pull/1354) From cf30b887974ae3555c00513a8aa9e05f553b4214 Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 10 Dec 2020 09:19:56 -0800 Subject: [PATCH 12/38] Apply Column and Row Styles to Existing Cells (#1721) * Apply Column and Row Styles to Existing Cells This is a fix for issue #1712. When a style is applied to an entire row or column, it is currently only effective for cells which don't already contain a value. The code needs to iterate through existing cells in the row/column in order to apply the style to them. This could be considered a breaking change, however, I believe that the change makes things operate as users would expect, and that the existing implementation is incomplete. The change also removes protected element conditionalStyles from the Style class. That element is an unused remnant, and can no longer be set or retrieved - methods getConditionalStyles and setConditionalStyles actually act on an element in the Worksheet class. Finally, additional tests are added so that Style, and in fact the entire Style directory, now has 100% test coverage. * Scrutinizer Changes Scrutinizer flagged 6 statements. 5 can be easily corrected. One is absolutely wrong (it thinks iterating through cells in column can return null). Let's see if we can satisfy it. * Remove Exception For CellIterator on Empty Row/Column For my first attempt at this change, which corrects a bug by updating styles for non-empty cells when a style is set on a row or column, I wished to make things more efficient by using setIterateOnlyExistingCells, something which the existing documentation recommends. This caused an exception to be generated when the row or column is empty. So I removed that part of the change while I researched what was going on. I have completed that research. The existing code does throw an exception when the row/column is empty and iterateOnlyExistingCells is true. However, that does not seem like a reasonable action. This situation is analagous to iterating over an empty array, and that action is legal and does not throw. The same should apply here. There were no tests for this situation, and now there are. I have added additional tests, and coverage for all of RowCellIterator, ColumnCellIterator, and CellIterator are all now 100%. Some of my new tests were added in new members, because the existing tests all relied on mocking, which was not the best choice for the new tests. One of the existing tests for RowCellIteratorTest (testSeekOutOfRange) was wrong; it issued the expected exception, but for the wrong reason. I have added an additional test to ensure that it fails "correctly". The existing documentation says that the default value for IterateOnlyExistingCells is true. In fact, the default value is false. I have corrected the documentation. * More Scrutinizer I believe its analysis is incorrect, but this should silence it. * DocBlock Correction ColumnCellIterator DocBlock for current indicated it could return null or Cell, but it can really return only Cell. This had caused Scrutinizer to complain earlier. * PHP8 Environment Appears to be Fixed Cosmetic change to Doc member. I suspect there is a way to rerun all the tests without another push, but I have been unable to figure out how. --- docs/topics/accessing-cells.md | 6 +- src/PhpSpreadsheet/Style/Style.php | 30 ++-- .../Worksheet/ColumnCellIterator.php | 13 +- .../Worksheet/RowCellIterator.php | 14 +- tests/PhpSpreadsheetTests/Style/StyleTest.php | 138 ++++++++++++++++++ .../Worksheet/ColumnCellIterator2Test.php | 75 ++++++++++ .../Worksheet/ColumnCellIteratorTest.php | 12 +- .../Worksheet/RowCellIterator2Test.php | 75 ++++++++++ .../Worksheet/RowCellIteratorTest.php | 15 +- 9 files changed, 342 insertions(+), 36 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/ColumnCellIterator2Test.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index edb71514a2..a777afc11b 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -422,8 +422,10 @@ foreach ($worksheet->getRowIterator() as $row) { $cellIterator = $row->getCellIterator(); $cellIterator->setIterateOnlyExistingCells(FALSE); // This loops through all cells, // even if a cell value is not set. - // By default, only cells that have a value - // set will be iterated. + // For 'TRUE', we loop through cells + // only when their value is set. + // If this method is not called, + // the default value is 'false'. foreach ($cellIterator as $cell) { echo '' . $cell->getValue() . diff --git a/src/PhpSpreadsheet/Style/Style.php b/src/PhpSpreadsheet/Style/Style.php index c1aa319e30..f7c1be23fc 100644 --- a/src/PhpSpreadsheet/Style/Style.php +++ b/src/PhpSpreadsheet/Style/Style.php @@ -42,13 +42,6 @@ class Style extends Supervisor */ protected $numberFormat; - /** - * Conditional styles. - * - * @var Conditional[] - */ - protected $conditionalStyles; - /** * Protection. * @@ -85,7 +78,6 @@ public function __construct($isSupervisor = false, $isConditional = false) parent::__construct($isSupervisor); // Initialise values - $this->conditionalStyles = []; $this->font = new Font($isSupervisor, $isConditional); $this->fill = new Fill($isSupervisor, $isConditional); $this->borders = new Borders($isSupervisor, $isConditional); @@ -212,6 +204,8 @@ public function applyFromArray(array $pStyles, $pAdvanced = true) $rangeEnd = Coordinate::coordinateFromString($rangeB); // Translate column into index + $rangeStart0 = $rangeStart[0]; + $rangeEnd0 = $rangeEnd[0]; $rangeStart[0] = Coordinate::columnIndexFromString($rangeStart[0]); $rangeEnd[0] = Coordinate::columnIndexFromString($rangeEnd[0]); @@ -361,6 +355,13 @@ public function applyFromArray(array $pStyles, $pAdvanced = true) for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; } + foreach ($this->getActiveSheet()->getColumnIterator($rangeStart0, $rangeEnd0) as $columnIterator) { + $cellIterator = $columnIterator->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $columnCell) { + $columnCell->getStyle()->applyFromArray($pStyles); + } + } break; case 'ROW': @@ -372,6 +373,13 @@ public function applyFromArray(array $pStyles, $pAdvanced = true) $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; } } + foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) { + $cellIterator = $rowIterator->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $rowCell) { + $rowCell->getStyle()->applyFromArray($pStyles); + } + } break; case 'CELL': @@ -599,18 +607,12 @@ public function setQuotePrefix($pValue) */ public function getHashCode() { - $hashConditionals = ''; - foreach ($this->conditionalStyles as $conditional) { - $hashConditionals .= $conditional->getHashCode(); - } - return md5( $this->fill->getHashCode() . $this->font->getHashCode() . $this->borders->getHashCode() . $this->alignment->getHashCode() . $this->numberFormat->getHashCode() . - $hashConditionals . $this->protection->getHashCode() . ($this->quotePrefix ? 't' : 'f') . __CLASS__ diff --git a/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php b/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php index 12420d77b9..714ee7ce3b 100644 --- a/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php @@ -92,10 +92,11 @@ public function resetEnd($endRow = null) */ public function seek($row = 1) { + if ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $row))) { + throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); + } if (($row < $this->startRow) || ($row > $this->endRow)) { throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})"); - } elseif ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $row))) { - throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); } $this->currentRow = $row; @@ -113,7 +114,7 @@ public function rewind(): void /** * Return the current cell in this worksheet column. * - * @return null|\PhpOffice\PhpSpreadsheet\Cell\Cell + * @return \PhpOffice\PhpSpreadsheet\Cell\Cell */ public function current() { @@ -180,18 +181,12 @@ protected function adjustForExistingOnlyRange(): void ) { ++$this->startRow; } - if ($this->startRow > $this->endRow) { - throw new PhpSpreadsheetException('No cells exist within the specified range'); - } while ( (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->endRow)) && ($this->endRow >= $this->startRow) ) { --$this->endRow; } - if ($this->endRow < $this->startRow) { - throw new PhpSpreadsheetException('No cells exist within the specified range'); - } } } } diff --git a/src/PhpSpreadsheet/Worksheet/RowCellIterator.php b/src/PhpSpreadsheet/Worksheet/RowCellIterator.php index f5576dc797..9b9d54eb61 100644 --- a/src/PhpSpreadsheet/Worksheet/RowCellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/RowCellIterator.php @@ -93,12 +93,14 @@ public function resetEnd($endColumn = null) */ public function seek($column = 'A') { + $columnx = $column; $column = Coordinate::columnIndexFromString($column); - if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) { - throw new PhpSpreadsheetException("Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"); - } elseif ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($column, $this->rowIndex))) { + if ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($column, $this->rowIndex))) { throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); } + if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) { + throw new PhpSpreadsheetException("Column $columnx is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"); + } $this->currentColumnIndex = $column; return $this; @@ -181,15 +183,9 @@ protected function adjustForExistingOnlyRange(): void while ((!$this->worksheet->cellExistsByColumnAndRow($this->startColumnIndex, $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) { ++$this->startColumnIndex; } - if ($this->startColumnIndex > $this->endColumnIndex) { - throw new PhpSpreadsheetException('No cells exist within the specified range'); - } while ((!$this->worksheet->cellExistsByColumnAndRow($this->endColumnIndex, $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) { --$this->endColumnIndex; } - if ($this->endColumnIndex < $this->startColumnIndex) { - throw new PhpSpreadsheetException('No cells exist within the specified range'); - } } } } diff --git a/tests/PhpSpreadsheetTests/Style/StyleTest.php b/tests/PhpSpreadsheetTests/Style/StyleTest.php index b695a50b38..6f1577099a 100644 --- a/tests/PhpSpreadsheetTests/Style/StyleTest.php +++ b/tests/PhpSpreadsheetTests/Style/StyleTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Style; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Fill; use PHPUnit\Framework\TestCase; class StyleTest extends TestCase @@ -19,4 +20,141 @@ public function testStyleOddMethods(): void $outArray = $cell1style->getStyleArray($styleArray); self::assertEquals($styleArray, $outArray['quotePrefix']); } + + public function testStyleColumn(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $cellCoordinates = 'A:B'; + $styleArray = [ + 'font' => [ + 'bold' => true, + ], + ]; + $sheet->getStyle($cellCoordinates)->applyFromArray($styleArray); + $sheet->setCellValue('A1', 'xxxa1'); + $sheet->setCellValue('A2', 'xxxa2'); + $sheet->setCellValue('A3', 'xxxa3'); + $sheet->setCellValue('B1', 'xxxa1'); + $sheet->setCellValue('B2', 'xxxa2'); + $sheet->setCellValue('B3', 'xxxa3'); + $sheet->setCellValue('C1', 'xxxc1'); + $sheet->setCellValue('C2', 'xxxc2'); + $sheet->setCellValue('C3', 'xxxc3'); + $styleArray = [ + 'font' => [ + 'italic' => true, + ], + ]; + $sheet->getStyle($cellCoordinates)->applyFromArray($styleArray); + self::assertTrue($sheet->getStyle('A1')->getFont()->getBold()); + self::assertTrue($sheet->getStyle('B2')->getFont()->getBold()); + self::assertFalse($sheet->getStyle('C3')->getFont()->getBold()); + self::assertTrue($sheet->getStyle('A1')->getFont()->getItalic()); + self::assertTrue($sheet->getStyle('B2')->getFont()->getItalic()); + self::assertFalse($sheet->getStyle('C3')->getFont()->getItalic()); + } + + public function testStyleRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $cellCoordinates = '2:3'; + $styleArray = [ + 'font' => [ + 'bold' => true, + ], + ]; + $sheet->getStyle($cellCoordinates)->applyFromArray($styleArray); + $sheet->setCellValue('A1', 'xxxa1'); + $sheet->setCellValue('A2', 'xxxa2'); + $sheet->setCellValue('A3', 'xxxa3'); + $sheet->setCellValue('B1', 'xxxa1'); + $sheet->setCellValue('B2', 'xxxa2'); + $sheet->setCellValue('B3', 'xxxa3'); + $sheet->setCellValue('C1', 'xxxc1'); + $sheet->setCellValue('C2', 'xxxc2'); + $sheet->setCellValue('C3', 'xxxc3'); + $styleArray = [ + 'font' => [ + 'italic' => true, + ], + ]; + $sheet->getStyle($cellCoordinates)->applyFromArray($styleArray); + self::assertFalse($sheet->getStyle('A1')->getFont()->getBold()); + self::assertTrue($sheet->getStyle('B2')->getFont()->getBold()); + self::assertTrue($sheet->getStyle('C3')->getFont()->getBold()); + self::assertFalse($sheet->getStyle('A1')->getFont()->getItalic()); + self::assertTrue($sheet->getStyle('B2')->getFont()->getItalic()); + self::assertTrue($sheet->getStyle('C3')->getFont()->getItalic()); + } + + public function testIssue1712A(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $rgb = '4467b8'; + $sheet->fromArray(['OK', 'KO']); + $spreadsheet->getActiveSheet() + ->getStyle('A1') + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB($rgb); + $spreadsheet->getActiveSheet() + ->getStyle('B') + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB($rgb); + self::assertEquals($rgb, $sheet->getCell('A1')->getStyle()->getFill()->getStartColor()->getRGB()); + self::assertEquals($rgb, $sheet->getCell('B1')->getStyle()->getFill()->getStartColor()->getRGB()); + } + + public function testIssue1712B(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $rgb = '4467b8'; + $spreadsheet->getActiveSheet() + ->getStyle('A1') + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB($rgb); + $spreadsheet->getActiveSheet() + ->getStyle('B') + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB($rgb); + $sheet->fromArray(['OK', 'KO']); + self::assertEquals($rgb, $sheet->getCell('A1')->getStyle()->getFill()->getStartColor()->getRGB()); + self::assertEquals($rgb, $sheet->getCell('B1')->getStyle()->getFill()->getStartColor()->getRGB()); + } + + public function testStyleLoopUpwards(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $cellCoordinates = 'C5:A3'; + $styleArray = [ + 'font' => [ + 'bold' => true, + ], + ]; + $sheet->getStyle($cellCoordinates)->applyFromArray($styleArray); + $sheet->setCellValue('A1', 'xxxa1'); + $sheet->setCellValue('A2', 'xxxa2'); + $sheet->setCellValue('A3', 'xxxa3'); + $sheet->setCellValue('B1', 'xxxa1'); + $sheet->setCellValue('B2', 'xxxa2'); + $sheet->setCellValue('B3', 'xxxa3'); + $sheet->setCellValue('C1', 'xxxc1'); + $sheet->setCellValue('C2', 'xxxc2'); + $sheet->setCellValue('C3', 'xxxc3'); + self::assertFalse($sheet->getStyle('A1')->getFont()->getBold()); + self::assertFalse($sheet->getStyle('B2')->getFont()->getBold()); + self::assertTrue($sheet->getStyle('C3')->getFont()->getBold()); + } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIterator2Test.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIterator2Test.php new file mode 100644 index 0000000000..c542d89e3b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIterator2Test.php @@ -0,0 +1,75 @@ +getActiveSheet(); + $sheet->getCell('B2')->setValue('cellb2'); + $sheet->getCell('B6')->setValue('cellb6'); + + $iterator = new ColumnCellIterator($sheet, 'B', 1, 8); + if (isset($existing)) { + $iterator->setIterateOnlyExistingCells($existing); + } + $lastCoordinate = ''; + $firstCoordinate = ''; + foreach ($iterator as $cell) { + $lastCoordinate = $cell->getCoordinate(); + if (!$firstCoordinate) { + $firstCoordinate = $lastCoordinate; + } + } + self::assertEquals($expectedResultFirst, $firstCoordinate); + self::assertEquals($expectedResultLast, $lastCoordinate); + } + + public function providerExistingCell(): array + { + return [ + [null, 'B1', 'B8'], + [false, 'B1', 'B8'], + [true, 'B2', 'B6'], + ]; + } + + /** + * @dataProvider providerEmptyColumn + */ + public function testEmptyColumn(?bool $existing, int $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('B2')->setValue('cellb2'); + $sheet->getCell('B6')->setValue('cellb6'); + + $iterator = new ColumnCellIterator($sheet, 'C'); + if (isset($existing)) { + $iterator->setIterateOnlyExistingCells($existing); + } + $numCells = 0; + foreach ($iterator as $cell) { + ++$numCells; + } + self::assertEquals($expectedResult, $numCells); + } + + public function providerEmptyColumn(): array + { + return [ + [null, 6], + [false, 6], + [true, 0], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php index 2083f347c4..1fa25330af 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php @@ -71,11 +71,21 @@ public function testIteratorSeekAndPrev(): void public function testSeekOutOfRange(): void { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - + $this->expectExceptionMessage('Row 1 is out of range'); $iterator = new ColumnCellIterator($this->mockWorksheet, 'A', 2, 4); $iterator->seek(1); } + public function testSeekNotExisting(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->expectExceptionMessage('Cell does not exist'); + + $iterator = new ColumnCellIterator($this->mockWorksheet, 'A', 2, 4); + $iterator->setIterateOnlyExistingCells(true); + $iterator->seek(2); + } + public function testPrevOutOfRange(): void { $iterator = new ColumnCellIterator($this->mockWorksheet, 'A', 2, 4); diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php b/tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php new file mode 100644 index 0000000000..20d10da988 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php @@ -0,0 +1,75 @@ +getActiveSheet(); + $sheet->getCell('C2')->setValue('cellb2'); + $sheet->getCell('F2')->setValue('cellf2'); + + $iterator = new RowCellIterator($sheet, 2, 'B', 'H'); + if (isset($existing)) { + $iterator->setIterateOnlyExistingCells($existing); + } + $lastCoordinate = ''; + $firstCoordinate = ''; + foreach ($iterator as $cell) { + $lastCoordinate = $cell->getCoordinate(); + if (!$firstCoordinate) { + $firstCoordinate = $lastCoordinate; + } + } + self::assertEquals($expectedResultFirst, $firstCoordinate); + self::assertEquals($expectedResultLast, $lastCoordinate); + } + + public function providerExistingCell(): array + { + return [ + [null, 'B2', 'H2'], + [false, 'B2', 'H2'], + [true, 'C2', 'F2'], + ]; + } + + /** + * @dataProvider providerEmptyRow + */ + public function testEmptyRow(?bool $existing, int $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('B2')->setValue('cellb2'); + $sheet->getCell('F2')->setValue('cellf2'); + + $iterator = new RowCellIterator($sheet, '3'); + if (isset($existing)) { + $iterator->setIterateOnlyExistingCells($existing); + } + $numCells = 0; + foreach ($iterator as $cell) { + ++$numCells; + } + self::assertEquals($expectedResult, $numCells); + } + + public function providerEmptyRow(): array + { + return [ + [null, 6], + [false, 6], + [true, 0], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php index bc2c16dc1d..4105c91c18 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php @@ -73,9 +73,22 @@ public function testIteratorSeekAndPrev(): void public function testSeekOutOfRange(): void { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->expectExceptionMessage('Column A is out of range'); $iterator = new RowCellIterator($this->mockWorksheet, 2, 'B', 'D'); - $iterator->seek(1); + self::assertFalse($iterator->getIterateOnlyExistingCells()); + self::assertEquals(2, $iterator->getCurrentColumnIndex()); + $iterator->seek('A'); + } + + public function testSeekNotExisting(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $this->expectExceptionMessage('Cell does not exist'); + + $iterator = new RowCellIterator($this->mockWorksheet, 2, 'B', 'D'); + $iterator->setIterateOnlyExistingCells(true); + $iterator->seek('B'); } public function testPrevOutOfRange(): void From ebae8ef811b1080abc9460d0bba194bda201db65 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 10 Dec 2020 18:20:18 +0100 Subject: [PATCH 13/38] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c818cb9358..f07a84d18a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Apply Column and Row Styles to Existing Cells [#1712](https://github.com/PHPOffice/PhpSpreadsheet/issues/1712) [PR #1721](https://github.com/PHPOffice/PhpSpreadsheet/pull/1721) - Resolve issues with defined names where worksheet doesn't exist (#1686)[https://github.com/PHPOffice/PhpSpreadsheet/issues/1686] and [#1723](https://github.com/PHPOffice/PhpSpreadsheet/issues/1723) - [PR #1742](https://github.com/PHPOffice/PhpSpreadsheet/pull/1742) - Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) - Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). From 2cbe2fd78ae965675c67ca04cd3ff6dc97185e6b Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 10 Dec 2020 09:35:26 -0800 Subject: [PATCH 14/38] TextData Coverage and Minor Bug Fixes (#1744) This had been intended to get 100% coverage for TextData functions, and it does that. However, some minor bugs requiring source changes arose during testing. - the Excel CHAR function restricts its argument to 1-255. PhpSpreadsheet CHARACTER had been allowing 0+. Also, there is no need to test if iconv exists, since it is part of Composer requirements. - The DOLLAR function had been returning NUM for invalid arguments. Excel returns VALUE. Also, negative amounts were not being handled correctly. - The FIXEDFORMAT function had been returning NUM for invalid arguments. Excel FIXED returns VALUE. --- src/PhpSpreadsheet/Calculation/TextData.php | 15 +++++----- tests/data/Calculation/TextData/CHAR.php | 28 ++++++++++++++---- tests/data/Calculation/TextData/DOLLAR.php | 18 ++++++++++-- tests/data/Calculation/TextData/FIXED.php | 32 +++++++++++++++++++-- tests/data/Calculation/TextData/SEARCH.php | 5 ++++ 5 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/TextData.php b/src/PhpSpreadsheet/Calculation/TextData.php index da958836ce..f89744029e 100644 --- a/src/PhpSpreadsheet/Calculation/TextData.php +++ b/src/PhpSpreadsheet/Calculation/TextData.php @@ -27,15 +27,15 @@ public static function CHARACTER($character) { $character = Functions::flattenSingleValue($character); - if ((!is_numeric($character)) || ($character < 0)) { + if (!is_numeric($character)) { return Functions::VALUE(); } - - if (function_exists('iconv')) { - return iconv('UCS-4LE', 'UTF-8', pack('V', $character)); + $character = (int) $character; + if ($character < 1 || $character > 255) { + return Functions::VALUE(); } - return mb_convert_encoding('&#' . (int) $character . ';', 'UTF-8', 'HTML-ENTITIES'); + return iconv('UCS-4LE', 'UTF-8', pack('V', $character)); } /** @@ -160,7 +160,7 @@ public static function DOLLAR($value = 0, $decimals = 2) // Validate parameters if (!is_numeric($value) || !is_numeric($decimals)) { - return Functions::NAN(); + return Functions::VALUE(); } $decimals = floor($decimals); @@ -174,6 +174,7 @@ public static function DOLLAR($value = 0, $decimals = 2) } $value = MathTrig::MROUND($value, $round); } + $mask = "$mask;($mask)"; return NumberFormat::toFormattedString($value, $mask); } @@ -265,7 +266,7 @@ public static function FIXEDFORMAT($value, $decimals = 2, $no_commas = false) // Validate parameters if (!is_numeric($value) || !is_numeric($decimals)) { - return Functions::NAN(); + return Functions::VALUE(); } $decimals = (int) floor($decimals); diff --git a/tests/data/Calculation/TextData/CHAR.php b/tests/data/Calculation/TextData/CHAR.php index 1751699c3d..276d701518 100644 --- a/tests/data/Calculation/TextData/CHAR.php +++ b/tests/data/Calculation/TextData/CHAR.php @@ -9,6 +9,10 @@ '#VALUE!', -5, ], + [ + '#VALUE!', + 0, + ], [ 'A', 65, @@ -22,27 +26,39 @@ 126, ], [ - '⽇', + 'Á', + 193, + ], + [ + 'ÿ', + 255, + ], + [ + '#VALUE!', + 256, + ], + [ + '#VALUE!', // '⽇', 12103, ], [ - 'œ', + '#VALUE!', // 'œ', 0x153, ], [ - 'ƒ', + '#VALUE!', // 'ƒ', 0x192, ], [ - '℅', + '#VALUE!', // '℅', 0x2105, ], [ - '∑', + '#VALUE!', // '∑', 0x2211, ], [ - '†', + '#VALUE!', // '†', 0x2020, ], ]; diff --git a/tests/data/Calculation/TextData/DOLLAR.php b/tests/data/Calculation/TextData/DOLLAR.php index 95555e7c4c..67f394bb72 100644 --- a/tests/data/Calculation/TextData/DOLLAR.php +++ b/tests/data/Calculation/TextData/DOLLAR.php @@ -6,11 +6,20 @@ 123.456, 2, ], + [ + '$123.46', + 123.456, + ], [ '$123.32', 123.321, 2, ], + [ + '($123.32)', + -123.321, + 2, + ], [ '$1,235,000', 1234567, @@ -22,12 +31,17 @@ -5, ], [ - '#NUM!', + '($1,200,000)', + -1234567, + -5, + ], + [ + '#VALUE!', 'ABC', 2, ], [ - '#NUM!', + '#VALUE!', 123.456, 'ABC', ], diff --git a/tests/data/Calculation/TextData/FIXED.php b/tests/data/Calculation/TextData/FIXED.php index b8bb36ca4b..2083eccebe 100644 --- a/tests/data/Calculation/TextData/FIXED.php +++ b/tests/data/Calculation/TextData/FIXED.php @@ -20,13 +20,41 @@ true, ], [ - '#NUM!', + '-123456.79', + -123456.789, + 2, + true, + ], + [ + '123500', + 123456.789, + -2, + true, + ], + [ + '123,500', + 123456.789, + -2, + ], + [ + '-123500', + -123456.789, + -2, + true, + ], + [ + '-123,500', + -123456.789, + -2, + ], + [ + '#VALUE!', 'ABC', 2, null, ], [ - '#NUM!', + '#VALUE!', 123.456, 'ABC', null, diff --git a/tests/data/Calculation/TextData/SEARCH.php b/tests/data/Calculation/TextData/SEARCH.php index 75aa7cea7d..28cd98f81e 100644 --- a/tests/data/Calculation/TextData/SEARCH.php +++ b/tests/data/Calculation/TextData/SEARCH.php @@ -54,6 +54,11 @@ 'Mark Baker', 2, ], + [ + 1, + '', + 'Mark Baker', + ], [ '#VALUE!', 'BITE', From a4bd396779e2dcaad3fadfc1dbc411adca870d07 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Thu, 10 Dec 2020 21:03:54 +0100 Subject: [PATCH 15/38] Replace anti-xss with html purifier (#1751) * Replace voku/anti-xss with ezyang/htmlpurifier. Despite anti-xss being a smaller footprint dependency, an a better license fit with our MIT license, there are issues with it's automatic it sanitisation of global variables causing side effects * Additional unit tests for xss in html writer cell comments --- composer.json | 2 +- composer.lock | 873 ++++++------------ src/PhpSpreadsheet/Writer/Html.php | 8 +- .../Writer/Html/XssVulnerabilityTest.php | 49 +- 4 files changed, 354 insertions(+), 578 deletions(-) diff --git a/composer.json b/composer.json index 7e3b840e46..c6f8e30e89 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "psr/simple-cache": "^1.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "voku/anti-xss": "^4.1" + "ezyang/htmlpurifier": "^4.13" }, "require-dev": { "dompdf/dompdf": "^0.8.5", diff --git a/composer.lock b/composer.lock index 5dd7483349..a1fb99b7c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "209605c0b9329968170279f40db65d22", + "content-hash": "458fe4e974b469230da589a8781d1e0e", "packages": [ + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/master" + }, + "time": "2020-06-29T00:56:53+00:00" + }, { "name": "maennchen/zipstream-php", "version": "2.1.0", @@ -477,242 +531,6 @@ ], "time": "2017-10-23T01:57:42+00:00" }, - { - "name": "symfony/polyfill-iconv", - "version": "v1.20.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/c536646fdb4f29104dd26effc2fdcb9a5b085024", - "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-iconv": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.20-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Iconv\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Iconv extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "iconv", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-23T14:02:19+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", - "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, { "name": "symfony/polyfill-mbstring", "version": "v1.18.1", @@ -753,311 +571,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "voku/anti-xss", - "version": "4.1.30", - "source": { - "type": "git", - "url": "https://github.com/voku/anti-xss.git", - "reference": "ff6e54f4a98ad1cd28f8b4a0f3c3f92f3c421f0a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/voku/anti-xss/zipball/ff6e54f4a98ad1cd28f8b4a0f3c3f92f3c421f0a", - "reference": "ff6e54f4a98ad1cd28f8b4a0f3c3f92f3c421f0a", - "shasum": "" - }, - "require": { - "php": ">=7.0.0", - "voku/portable-utf8": "~5.4.50" - }, - "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "voku\\helper\\": "src/voku/helper/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "EllisLab Dev Team", - "homepage": "http://ellislab.com/" - }, - { - "name": "Lars Moelleken", - "email": "lars@moelleken.org", - "homepage": "http://www.moelleken.org/" - } - ], - "description": "anti xss-library", - "homepage": "https://github.com/voku/anti-xss", - "keywords": [ - "anti-xss", - "clean", - "security", - "xss" - ], - "funding": [ - { - "url": "https://www.paypal.me/moelleken", - "type": "custom" - }, - { - "url": "https://github.com/voku", - "type": "github" - }, - { - "url": "https://opencollective.com/anti-xss", - "type": "open_collective" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/voku/anti-xss", - "type": "tidelift" - } - ], - "time": "2020-11-12T00:30:57+00:00" - }, - { - "name": "voku/portable-ascii", - "version": "1.5.6", - "source": { - "type": "git", - "url": "https://github.com/voku/portable-ascii.git", - "reference": "80953678b19901e5165c56752d087fc11526017c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/80953678b19901e5165c56752d087fc11526017c", - "reference": "80953678b19901e5165c56752d087fc11526017c", - "shasum": "" - }, - "require": { - "php": ">=7.0.0" - }, - "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" - }, - "suggest": { - "ext-intl": "Use Intl for transliterator_transliterate() support" - }, - "type": "library", - "autoload": { - "psr-4": { - "voku\\": "src/voku/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/" - } - ], - "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", - "homepage": "https://github.com/voku/portable-ascii", - "keywords": [ - "ascii", - "clean", - "php" - ], - "funding": [ - { - "url": "https://www.paypal.me/moelleken", - "type": "custom" - }, - { - "url": "https://github.com/voku", - "type": "github" - }, - { - "url": "https://opencollective.com/portable-ascii", - "type": "open_collective" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", - "type": "tidelift" - } - ], - "time": "2020-11-12T00:07:28+00:00" - }, - { - "name": "voku/portable-utf8", - "version": "5.4.50", - "source": { - "type": "git", - "url": "https://github.com/voku/portable-utf8.git", - "reference": "f14ed68ea9ced6639e71ca989c6d907892115ba0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/voku/portable-utf8/zipball/f14ed68ea9ced6639e71ca989c6d907892115ba0", - "reference": "f14ed68ea9ced6639e71ca989c6d907892115ba0", - "shasum": "" - }, - "require": { - "php": ">=7.0.0", - "symfony/polyfill-iconv": "~1.0", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php72": "~1.0", - "voku/portable-ascii": "~1.5.6" - }, - "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" - }, - "suggest": { - "ext-ctype": "Use Ctype for e.g. hexadecimal digit detection", - "ext-fileinfo": "Use Fileinfo for better binary file detection", - "ext-iconv": "Use iconv for best performance", - "ext-intl": "Use Intl for best performance", - "ext-json": "Use JSON for string detection", - "ext-mbstring": "Use Mbstring for best performance" - }, - "type": "library", - "autoload": { - "psr-4": { - "voku\\": "src/voku/" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "(Apache-2.0 or GPL-2.0)" + "MIT" ], "authors": [ { @@ -1065,47 +579,34 @@ "email": "p@tchwork.com" }, { - "name": "Hamid Sarfraz", - "homepage": "http://pageconfig.com/" - }, - { - "name": "Lars Moelleken", - "homepage": "http://www.moelleken.org/" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Portable UTF-8 library - performance optimized (unicode) string functions for php.", - "homepage": "https://github.com/voku/portable-utf8", + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", "keywords": [ - "UTF", - "clean", - "php", - "unicode", - "utf-8", - "utf8" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "funding": [ { - "url": "https://www.paypal.me/moelleken", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://github.com/voku", + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://opencollective.com/portable-utf8", - "type": "open_collective" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/voku/portable-utf8", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2020-11-12T00:17:47+00:00" + "time": "2020-07-14T12:35:20+00:00" } ], "packages-dev": [ @@ -4150,6 +3651,165 @@ ], "time": "2020-07-14T12:35:20+00:00" }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, { "name": "symfony/polyfill-php70", "version": "v1.18.1", @@ -4227,6 +3887,79 @@ ], "time": "2020-07-14T12:35:20+00:00" }, + { + "name": "symfony/polyfill-php72", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "639447d008615574653fb3bc60d1986d7172eaae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", + "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, { "name": "symfony/polyfill-php73", "version": "v1.18.1", @@ -4836,7 +4569,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.2|^8.0", + "php": "^7.2||^8.0", "ext-ctype": "*", "ext-dom": "*", "ext-gd": "*", @@ -4852,5 +4585,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 31cc05afe8..e9de2ce688 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer; +use HTMLPurifier; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; @@ -23,7 +24,6 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use voku\helper\AntiXSS; class Html extends BaseWriter { @@ -1789,9 +1789,9 @@ private function writeComment(Worksheet $pSheet, $coordinate) { $result = ''; if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) { - $sanitizer = new AntiXSS(); - $sanitizedString = $sanitizer->xss_clean($pSheet->getComment($coordinate)->getText()->getPlainText()); - if (!$sanitizer->isXssFound()) { + $sanitizer = new HTMLPurifier(); + $sanitizedString = $sanitizer->purify($pSheet->getComment($coordinate)->getText()->getPlainText()); + if ($sanitizedString !== '') { $result .= ''; $result .= '
' . nl2br($sanitizedString) . '
'; $result .= PHP_EOL; diff --git a/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php b/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php index 30d7af63f4..48aced0249 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/XssVulnerabilityTest.php @@ -10,15 +10,56 @@ class XssVulnerabilityTest extends Functional\AbstractFunctional { + public function providerAcceptableMarkupRichText() + { + return [ + 'basic text' => ['Hello, I am safely viewing your site', 'Hello, I am safely viewing your site'], + 'link' => ["Google is here", 'Google is here'], + ]; + } + + /** + * @dataProvider providerAcceptableMarkupRichText + * + * @param string $safeTextString + * @param string $adjustedTextString + */ + public function testMarkupInComment($safeTextString, $adjustedTextString): void + { + $spreadsheet = new Spreadsheet(); + + $richText = new RichText(); + $richText->createText($safeTextString); + + $spreadsheet->getActiveSheet()->getCell('A1')->setValue('XSS Test'); + + $spreadsheet->getActiveSheet() + ->getComment('A1') + ->setText($richText); + + $filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + + $writer = IOFactory::createWriter($spreadsheet, 'Html'); + $writer->save($filename); + + $verify = file_get_contents($filename); + // Ensure that executable js has been stripped from the comments + self::assertStringContainsString($adjustedTextString, $verify); + } + public function providerXssRichText() { return [ - 'script tag' => [''], - 'javascript tag' => ['javascript:alert(1)'], - 'with unicode' => ['java\u0003script:alert(1)'], + 'script tag' => ["Hello, I am trying to your site"], + 'javascript tag' => ["CLICK"], + 'with unicode' => ['CLICK'], + 'inline css' => ['
  • '], + 'char value chevron' => ["\x3cscript src=http://www.example.com/malicious-code.js\x3e\x3c/script\x3e"], ]; } + private static $counter = 0; + /** * @dataProvider providerXssRichText * @@ -43,6 +84,8 @@ public function testXssInComment($xssTextString): void $writer->save($filename); $verify = file_get_contents($filename); + $counter = self::$counter++; + file_put_contents("verify{$counter}.html", $verify); // Ensure that executable js has been stripped from the comments self::assertStringNotContainsString($xssTextString, $verify); } From dfc0ff4fca6e867329f361ea506865ba0d6e7445 Mon Sep 17 00:00:00 2001 From: Flinsch <220455+Flinsch@users.noreply.github.com> Date: Thu, 10 Dec 2020 21:49:53 +0100 Subject: [PATCH 16/38] Fix bug #1626 where values of 0 were "rounded" up/down as if they were not 0 (#1627) * Fix bug where values of 0 were "rounded" up/down as if they were not 0 --- CHANGELOG.md | 3 ++- src/PhpSpreadsheet/Calculation/MathTrig.php | 8 ++++++++ tests/data/Calculation/MathTrig/ROUNDDOWN.php | 5 +++++ tests/data/Calculation/MathTrig/ROUNDUP.php | 5 +++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07a84d18a..1132663839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- PrintArea causes exception [#1544](https://github.com/phpoffice/phpspreadsheet/pull/1544) +- PrintArea causes exception [#1544](https://github.com/phpoffice/phpspreadsheet/pull/1544) +- ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) - Calculation/DateTime Failure With PHP8 [#1661](https://github.com/phpoffice/phpspreadsheet/pull/1661) - Reader/Gnumeric Failure with PHP8 [#1662](https://github.com/phpoffice/phpspreadsheet/pull/1662) - ReverseSort bug, exposed but not caused by PHP8 [#1660](https://github.com/phpoffice/phpspreadsheet/pull/1660) diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index 7539659ec2..823f6ef2c8 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -1108,6 +1108,10 @@ public static function ROUNDUP($number, $digits) $digits = Functions::flattenSingleValue($digits); if ((is_numeric($number)) && (is_numeric($digits))) { + if ($number == 0.0) { + return 0.0; + } + if ($number < 0.0) { return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } @@ -1134,6 +1138,10 @@ public static function ROUNDDOWN($number, $digits) $digits = Functions::flattenSingleValue($digits); if ((is_numeric($number)) && (is_numeric($digits))) { + if ($number == 0.0) { + return 0.0; + } + if ($number < 0.0) { return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } diff --git a/tests/data/Calculation/MathTrig/ROUNDDOWN.php b/tests/data/Calculation/MathTrig/ROUNDDOWN.php index b1b3ac71f8..1c2c67e1ad 100644 --- a/tests/data/Calculation/MathTrig/ROUNDDOWN.php +++ b/tests/data/Calculation/MathTrig/ROUNDDOWN.php @@ -1,6 +1,11 @@ Date: Thu, 10 Dec 2020 21:51:24 +0100 Subject: [PATCH 17/38] Update change log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1132663839..774590df9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) - Apply Column and Row Styles to Existing Cells [#1712](https://github.com/PHPOffice/PhpSpreadsheet/issues/1712) [PR #1721](https://github.com/PHPOffice/PhpSpreadsheet/pull/1721) - Resolve issues with defined names where worksheet doesn't exist (#1686)[https://github.com/PHPOffice/PhpSpreadsheet/issues/1686] and [#1723](https://github.com/PHPOffice/PhpSpreadsheet/issues/1723) - [PR #1742](https://github.com/PHPOffice/PhpSpreadsheet/pull/1742) - Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) @@ -82,7 +83,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - PrintArea causes exception [#1544](https://github.com/phpoffice/phpspreadsheet/pull/1544) -- ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) - Calculation/DateTime Failure With PHP8 [#1661](https://github.com/phpoffice/phpspreadsheet/pull/1661) - Reader/Gnumeric Failure with PHP8 [#1662](https://github.com/phpoffice/phpspreadsheet/pull/1662) - ReverseSort bug, exposed but not caused by PHP8 [#1660](https://github.com/phpoffice/phpspreadsheet/pull/1660) From 201817c6d1e364765f71d6ca811fa66d3165a36c Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 10 Dec 2020 13:02:36 -0800 Subject: [PATCH 18/38] Fix for #1612 - SLK Long File Name (#1706) Issue has been marked stale, but ... Sylk read sets worksheet title to filename (minus .slk). If that is >31 characters, PhpSpreadsheet throws Exception. This change truncates sheet title, as Excel does, to 31 characters. --- src/PhpSpreadsheet/Reader/Slk.php | 3 ++- tests/PhpSpreadsheetTests/Reader/SlkTest.php | 24 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index 0e147376b6..e58ff2f6c5 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Border; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Slk extends BaseReader { @@ -516,7 +517,7 @@ public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) $spreadsheet->createSheet(); } $spreadsheet->setActiveSheetIndex($this->sheetIndex); - $spreadsheet->getActiveSheet()->setTitle(basename($pFilename, '.slk')); + $spreadsheet->getActiveSheet()->setTitle(substr(basename($pFilename, '.slk'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH)); // Loop through file $column = $row = ''; diff --git a/tests/PhpSpreadsheetTests/Reader/SlkTest.php b/tests/PhpSpreadsheetTests/Reader/SlkTest.php index 4c7cc5139a..e461557eba 100644 --- a/tests/PhpSpreadsheetTests/Reader/SlkTest.php +++ b/tests/PhpSpreadsheetTests/Reader/SlkTest.php @@ -4,6 +4,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Slk; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; @@ -12,6 +13,16 @@ class SlkTest extends \PHPUnit\Framework\TestCase { private static $testbook = __DIR__ . '/../../../samples/templates/SylkTest.slk'; + private $filename = ''; + + protected function teardown(): void + { + if ($this->filename) { + unlink($this->filename); + $this->filename = ''; + } + } + public function testInfo(): void { $reader = new Slk(); @@ -131,4 +142,17 @@ public function testSheetIndex(): void self::assertEquals('FFFF0000', $sheet->getCell('A1')->getStyle()->getFont()->getColor()->getARGB()); } + + public function testLongName(): void + { + $contents = file_get_contents(self::$testbook); + $this->filename = File::sysGetTempDir() + . '/123456789a123456789b123456789c12345.slk'; + file_put_contents($this->filename, $contents); + $reader = new Slk(); + $spreadsheet = $reader->load($this->filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('123456789a123456789b123456789c1', $sheet->getTitle()); + self::assertEquals('FFFF0000', $sheet->getCell('A1')->getStyle()->getFont()->getColor()->getARGB()); + } } From 5079aac2104e2d12bca8b4407c33a25a21e69e03 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 10 Dec 2020 22:04:00 +0100 Subject: [PATCH 19/38] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 774590df9f..2eab866f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Resolve issue with SLK long filenames [#1612](https://github.com/PHPOffice/PhpSpreadsheet/issues/1612) - ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) - Apply Column and Row Styles to Existing Cells [#1712](https://github.com/PHPOffice/PhpSpreadsheet/issues/1712) [PR #1721](https://github.com/PHPOffice/PhpSpreadsheet/pull/1721) - Resolve issues with defined names where worksheet doesn't exist (#1686)[https://github.com/PHPOffice/PhpSpreadsheet/issues/1686] and [#1723](https://github.com/PHPOffice/PhpSpreadsheet/issues/1723) - [PR #1742](https://github.com/PHPOffice/PhpSpreadsheet/pull/1742) From 6e56c2cb9cb6d58ef1b7fe8c16b90a6bd26f1162 Mon Sep 17 00:00:00 2001 From: Jan Sverre Riksfjord Date: Thu, 10 Dec 2020 22:52:00 +0100 Subject: [PATCH 20/38] worksheet: fix if cellValue does not exist (#1727) The condition is FALSE if the cell does not exist in the flipped table, but anyway, it is sent in to a method requiring 'string' type, causing it to fail. --- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index b6a6fc390b..8faa7ae259 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1079,7 +1079,7 @@ private function writeCellString(XMLWriter $objWriter, string $mappedType, $cell { $objWriter->writeAttribute('t', $mappedType); if (!$cellValue instanceof RichText) { - self::writeElementIf($objWriter, isset($pFlippedStringTable[$cellValue]), 'v', $pFlippedStringTable[$cellValue]); + self::writeElementIf($objWriter, isset($pFlippedStringTable[$cellValue]), 'v', $pFlippedStringTable[$cellValue] ?? ''); } else { $objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]); } From be3b30b4b1515311bc8dbc62561d2195b3596497 Mon Sep 17 00:00:00 2001 From: Max Kalyabin Date: Fri, 11 Dec 2020 01:10:01 +0300 Subject: [PATCH 21/38] fixes #1655 issue (#1656) Resolve problem with incorrectly defined hyperlinks --- src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php index 9e6aeaf788..106fd44efc 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -41,7 +41,7 @@ private function setHyperlink(SimpleXMLElement $hyperlink, Worksheet $worksheet) foreach (Coordinate::extractAllCellReferencesInRange($hyperlink['ref']) as $cellReference) { $cell = $worksheet->getCell($cellReference); if (isset($linkRel['id'])) { - $hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']]; + $hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']] ?? null; if (isset($hyperlink['location'])) { $hyperlinkUrl .= '#' . (string) $hyperlink['location']; } From fe45f08a4ec758b9c3fc2d45fc7fd862874d578b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Despont?= Date: Thu, 10 Dec 2020 23:28:46 +0100 Subject: [PATCH 22/38] Add 'ps' suffix to printer settings resources IDs (#1690) * Add 'ps' suffix to printer settings resources IDs --- src/PhpSpreadsheet/Reader/Xlsx.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 5dc48ff830..124cc3b252 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1971,7 +1971,7 @@ private function readPrinterSettings(Spreadsheet $excel, ZipArchive $zip, $dir, $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings']; foreach ($sheetPrinterSettings as $rId => $printerSettings) { - $rId = substr($rId, 3); // rIdXXX + $rId = substr($rId, 3) . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing $unparsedPrinterSettings[$rId] = []; $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $printerSettings['Target']); $unparsedPrinterSettings[$rId]['relFilePath'] = (string) $printerSettings['Target']; From b2b1f2588e20a0f0cbe2d708ecd689e99a782aa1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 10 Dec 2020 23:37:20 +0100 Subject: [PATCH 23/38] Update change log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eab866f13..df2a80ab7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Resolve Xlsx loader issue whe hyperlinks don't have a destination +- Resolve issues when printer settings resources IDs clash with drawing IDs - Resolve issue with SLK long filenames [#1612](https://github.com/PHPOffice/PhpSpreadsheet/issues/1612) - ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) - Apply Column and Row Styles to Existing Cells [#1712](https://github.com/PHPOffice/PhpSpreadsheet/issues/1712) [PR #1721](https://github.com/PHPOffice/PhpSpreadsheet/pull/1721) From 64a22e08e42354f4ccebbe81477f6b9854b0381e Mon Sep 17 00:00:00 2001 From: Guilliam Xavier Date: Thu, 10 Dec 2020 23:46:56 +0100 Subject: [PATCH 24/38] Fix pixelsToPoints conversion (for HTML col width) (#1733) --- src/PhpSpreadsheet/Shared/Drawing.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Shared/Drawing.php b/src/PhpSpreadsheet/Shared/Drawing.php index 25d6910d34..4ff89595db 100644 --- a/src/PhpSpreadsheet/Shared/Drawing.php +++ b/src/PhpSpreadsheet/Shared/Drawing.php @@ -98,7 +98,7 @@ public static function cellDimensionToPixels($pValue, \PhpOffice\PhpSpreadsheet\ */ public static function pixelsToPoints($pValue) { - return $pValue * 0.67777777; + return $pValue * 0.75; } /** @@ -111,7 +111,7 @@ public static function pixelsToPoints($pValue) public static function pointsToPixels($pValue) { if ($pValue != 0) { - return (int) ceil($pValue * 1.333333333); + return (int) ceil($pValue / 0.75); } return 0; From f011d1f3a9464e64b93ef0af8bc7aa827eca45ae Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 10 Dec 2020 15:20:09 -0800 Subject: [PATCH 25/38] DocBlock Change in Styles/Conditional (#1697) Scrutinizer reported a minor error in a test involving a module which I was not changing. Styles/Conditional function setConditions can take a scalar or an array as a parameter, but DocBlock says it only expects array. I did not wish to add the extra module to my PR, but made a note to self to fix that after PR was installed. That has now happened, and it makes for a good case for me to see all the PHP8/Composer2/etc. changes that have happened recently. --- src/PhpSpreadsheet/Style/Conditional.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index 35ec479ba5..e4fe0acc7f 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -189,7 +189,7 @@ public function getConditions() /** * Set Conditions. * - * @param string[] $pValue Condition + * @param bool|float|int|string|string[] $pValue Condition * * @return $this */ From c1a63d7013992b531f709ba4d9c8e0632f9c6446 Mon Sep 17 00:00:00 2001 From: oleibman Date: Thu, 17 Dec 2020 08:00:19 -0800 Subject: [PATCH 26/38] Merge pull request #1698 * Merge pull request #4 from PHPOffice/master * Restore Omitted Read XML Test --- src/PhpSpreadsheet/Reader/Xml.php | 8 ++- .../Reader/Xml/XmlOddTest.php | 70 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/XmlOddTest.php diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index e4d251e28f..11aa1df3bd 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -617,9 +617,11 @@ public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) ++$rowID; } - $xmlX = $worksheet->children($namespaces['x']); - if (isset($xmlX->WorksheetOptions)) { - (new PageSettings($xmlX, $namespaces))->loadPageSettings($spreadsheet); + if (isset($namespaces['x'])) { + $xmlX = $worksheet->children($namespaces['x']); + if (isset($xmlX->WorksheetOptions)) { + (new PageSettings($xmlX, $namespaces))->loadPageSettings($spreadsheet); + } } } ++$worksheetID; diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlOddTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlOddTest.php new file mode 100644 index 0000000000..e0b43113b1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlOddTest.php @@ -0,0 +1,70 @@ +filename) { + unlink($this->filename); + $this->filename = ''; + } + } + + public function testWriteThenRead(): void + { + $xmldata = <<< 'EOT' + + + + Xml2003 Short Workbook + + + 2 + + + 9000 + 13860 + 240 + 75 + False + False + + + + + + + Test String 1 + + +
    +
    +
    +EOT; + $this->filename = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + file_put_contents($this->filename, $xmldata); + $reader = new Xml(); + $spreadsheet = $reader->load($this->filename); + self::assertEquals(1, $spreadsheet->getSheetCount()); + + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('Sample Data', $sheet->getTitle()); + self::assertEquals('Test String 1', $sheet->getCell('A8')->getValue()); + + $props = $spreadsheet->getProperties(); + self::assertEquals('Xml2003 Short Workbook', $props->getTitle()); + self::assertEquals('2', $props->getCustomPropertyValue('myאInt')); + } +} From 65b86253d958d2be07d4e70e3ba5eba4d0d9103a Mon Sep 17 00:00:00 2001 From: Gianluca Giovinazzo Date: Thu, 17 Dec 2020 19:41:07 +0100 Subject: [PATCH 27/38] Fix for bug #1592 (UPDATED) (#1623) * Fix for Xls when BIFF8 SST (FCh) has bad Shared string length --- src/PhpSpreadsheet/Reader/Xls.php | 16 +++++++-- tests/PhpSpreadsheetTests/Reader/XlsTest.php | 35 +++++++++++++++++++ tests/data/Reader/XLS/bug1592.xls | Bin 0 -> 20992 bytes 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/data/Reader/XLS/bug1592.xls diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index b5c92d8d2a..023806d6c2 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -2973,6 +2973,9 @@ private function readSst(): void // offset within (spliced) record data $pos = 0; + // Limit global SST position, further control for bad SST Length in BIFF8 data + $limitposSST = 0; + // get spliced record data $splicedRecordData = $this->getSplicedRecordData(); @@ -2986,8 +2989,17 @@ private function readSst(): void $nm = self::getInt4d($recordData, 4); $pos += 4; + // look up limit position + foreach ($spliceOffsets as $spliceOffset) { + // it can happen that the string is empty, therefore we need + // <= and not just < + if ($pos <= $spliceOffset) { + $limitposSST = $spliceOffset; + } + } + // loop through the Unicode strings (16-bit length) - for ($i = 0; $i < $nm; ++$i) { + for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) { // number of characters in the Unicode string $numChars = self::getUInt2d($recordData, $pos); $pos += 2; @@ -3020,7 +3032,7 @@ private function readSst(): void // expected byte length of character array if not split $len = ($isCompressed) ? $numChars : $numChars * 2; - // look up limit position + // look up limit position - Check it again to be sure that no error occurs when parsing SST structure foreach ($spliceOffsets as $spliceOffset) { // it can happen that the string is empty, therefore we need // <= and not just < diff --git a/tests/PhpSpreadsheetTests/Reader/XlsTest.php b/tests/PhpSpreadsheetTests/Reader/XlsTest.php index da39f8b2a3..130374b359 100644 --- a/tests/PhpSpreadsheetTests/Reader/XlsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/XlsTest.php @@ -43,4 +43,39 @@ public function testLoadXlsBug1505(): void self::assertEquals($sheet->getCell('A1')->getFormattedValue(), $newsheet->getCell('A1')->getFormattedValue()); self::assertEquals($sheet->getCell("$col$row")->getFormattedValue(), $newsheet->getCell("$col$row")->getFormattedValue()); } + + /** + * Test load Xls file with invalid length in SST map. + */ + public function testLoadXlsBug1592(): void + { + $filename = 'tests/data/Reader/XLS/bug1592.xls'; + $reader = new Xls(); + // When no fix applied, spreadsheet is not loaded + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $col = $sheet->getHighestColumn(); + $row = $sheet->getHighestRow(); + + $newspreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $newsheet = $newspreadsheet->getActiveSheet(); + $newcol = $newsheet->getHighestColumn(); + $newrow = $newsheet->getHighestRow(); + + self::assertEquals($spreadsheet->getSheetCount(), $newspreadsheet->getSheetCount()); + self::assertEquals($sheet->getTitle(), $newsheet->getTitle()); + self::assertEquals($sheet->getColumnDimensions(), $newsheet->getColumnDimensions()); + self::assertEquals($col, $newcol); + self::assertEquals($row, $newrow); + + $rowIterator = $sheet->getRowIterator(); + + foreach ($rowIterator as $row) { + foreach ($row->getCellIterator() as $cell) { + $valOld = $cell->getFormattedValue(); + $valNew = $newsheet->getCell($cell->getCoordinate())->getFormattedValue(); + self::assertEquals($valOld, $valNew); + } + } + } } diff --git a/tests/data/Reader/XLS/bug1592.xls b/tests/data/Reader/XLS/bug1592.xls new file mode 100644 index 0000000000000000000000000000000000000000..02413abcc2d884a2cb2d12810dbd849d0860f4fe GIT binary patch literal 20992 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 Date: Thu, 17 Dec 2020 19:43:02 +0100 Subject: [PATCH 28/38] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2a80ab7b..1f9c85fe0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix for Xls Reader when SST has a bad length [#1592](https://github.com/PHPOffice/PhpSpreadsheet/issues/1592) - Resolve Xlsx loader issue whe hyperlinks don't have a destination - Resolve issues when printer settings resources IDs clash with drawing IDs - Resolve issue with SLK long filenames [#1612](https://github.com/PHPOffice/PhpSpreadsheet/issues/1612) From 99e023feba4f51565e0973487162d2ce493b6483 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 21 Dec 2020 17:16:19 +0100 Subject: [PATCH 29/38] Add nightly PHP 8.1 dev to github actions (#1763) --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4e13a6516..a01892aec3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ jobs: - '7.3' - '7.4' - '8.0' + - '8.1' name: PHP ${{ matrix.php-version }} @@ -37,7 +38,7 @@ jobs: - name: Delete composer lock file id: composer-lock - if: ${{ matrix.php-version == '8.0' }} + if: ${{ matrix.php-version == '8.0' || matrix.php-version == '8.1' }} run: | rm composer.lock echo "::set-output name=flags::--ignore-platform-reqs" From a70c1eb8c4d35bae850f764d741bea8e4f5c306b Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Fri, 25 Dec 2020 17:22:31 +0100 Subject: [PATCH 30/38] Fix compatibility with ext-gd on php 8 (#1762) --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Shared/Drawing.php | 4 +++- src/PhpSpreadsheet/Worksheet/MemoryDrawing.php | 8 +++++--- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 5 +++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f9c85fe0c..27fdd7180a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) - Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). - Fix for notice during accessing "cached magnification factor" offset [#1354](https://github.com/PHPOffice/PhpSpreadsheet/pull/1354) +- Fix compatibility with ext-gd on php 8 ### Security Fix (CVE-2020-7776) diff --git a/src/PhpSpreadsheet/Shared/Drawing.php b/src/PhpSpreadsheet/Shared/Drawing.php index 4ff89595db..f41fb695c4 100644 --- a/src/PhpSpreadsheet/Shared/Drawing.php +++ b/src/PhpSpreadsheet/Shared/Drawing.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use GdImage; + class Drawing { /** @@ -152,7 +154,7 @@ public static function angleToDegrees($pValue) * * @param string $p_sFile Path to Windows DIB (BMP) image * - * @return resource + * @return GdImage|resource */ public static function imagecreatefrombmp($p_sFile) { diff --git a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index 22e09099d4..fb002114fd 100644 --- a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use GdImage; + class MemoryDrawing extends BaseDrawing { // Rendering functions @@ -19,7 +21,7 @@ class MemoryDrawing extends BaseDrawing /** * Image resource. * - * @var resource + * @var GdImage|resource */ private $imageResource; @@ -62,7 +64,7 @@ public function __construct() /** * Get image resource. * - * @return resource + * @return GdImage|resource */ public function getImageResource() { @@ -72,7 +74,7 @@ public function getImageResource() /** * Set image resource. * - * @param resource $value + * @param GdImage|resource $value * * @return $this */ diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index a1c258c042..a793128ae0 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 GdImage; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; @@ -2254,7 +2255,7 @@ private function writePassword(): void */ public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void { - $bitmap_array = (is_resource($bitmap) ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); + $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); [$width, $height, $size, $data] = $bitmap_array; // Scale the frame of the image. @@ -2460,7 +2461,7 @@ private function writeObjPicture($colL, $dxL, $rwT, $dyT, $colR, $dxR, $rwB, $dy /** * Convert a GD-image into the internal format. * - * @param resource $image The image to process + * @param GdImage|resource $image The image to process * * @return array Array with data and properties of the bitmap */ From b69144397aa2beceabe544039b5f981c5ec344b3 Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 25 Dec 2020 08:47:29 -0800 Subject: [PATCH 31/38] CSV - Guess Encoding, Handle Null-string Escape (#1717) * CSV - Guess Encoding, Handle Null-string Escape This is in response to issue #1647 (detect CSV character encoding). First, my tests with mb_detect_encoding indicate that it doesn't work well enough; regardless, users can always do that on their own if they deem it useful. Rolling my own is also troublesome, but I can at least: a. Check for BOM (UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE). b. Do some heuristic tests for each of the above encodings. c. Fallback to a user-specified encoding (default CP1252) if a and b don't yield result. I think this is probably useful enough to include, and relatively easy to expand if other potential encodings should be considered. Starting with PHP7.4, fgetcsv allows specification of null string as escape character in fgetcsv. This is a much better choice than the PHP (and PhpSpreadsheet) default of backslash in that it handles the file in the same manner as Excel does. There is one statement in Reader/CSV which would be adversely affected if the caller so specified (building a regular expression under the assumption that escape character is a single character). Fix that statement appropriately and add tests. --- docs/topics/reading-and-writing-to-file.md | 18 ++++ src/PhpSpreadsheet/Reader/Csv.php | 88 ++++++++++++++++-- tests/PhpSpreadsheetTests/Reader/CsvTest.php | 62 ++++++++++++ tests/data/Reader/CSV/escape.csv | 4 + tests/data/Reader/CSV/premiere.utf16be.csv | Bin 0 -> 112 bytes tests/data/Reader/CSV/premiere.utf16bebom.csv | Bin 0 -> 114 bytes tests/data/Reader/CSV/premiere.utf16le.csv | Bin 0 -> 112 bytes tests/data/Reader/CSV/premiere.utf16lebom.csv | Bin 0 -> 114 bytes tests/data/Reader/CSV/premiere.utf32be.csv | Bin 0 -> 224 bytes tests/data/Reader/CSV/premiere.utf32bebom.csv | Bin 0 -> 228 bytes tests/data/Reader/CSV/premiere.utf32le.csv | Bin 0 -> 224 bytes tests/data/Reader/CSV/premiere.utf32lebom.csv | Bin 0 -> 228 bytes tests/data/Reader/CSV/premiere.utf8.csv | 2 + tests/data/Reader/CSV/premiere.utf8bom.csv | 2 + tests/data/Reader/CSV/premiere.win1252.csv | 2 + 15 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 tests/data/Reader/CSV/escape.csv create mode 100644 tests/data/Reader/CSV/premiere.utf16be.csv create mode 100644 tests/data/Reader/CSV/premiere.utf16bebom.csv create mode 100644 tests/data/Reader/CSV/premiere.utf16le.csv create mode 100644 tests/data/Reader/CSV/premiere.utf16lebom.csv create mode 100644 tests/data/Reader/CSV/premiere.utf32be.csv create mode 100644 tests/data/Reader/CSV/premiere.utf32bebom.csv create mode 100644 tests/data/Reader/CSV/premiere.utf32le.csv create mode 100644 tests/data/Reader/CSV/premiere.utf32lebom.csv create mode 100644 tests/data/Reader/CSV/premiere.utf8.csv create mode 100644 tests/data/Reader/CSV/premiere.utf8bom.csv create mode 100644 tests/data/Reader/CSV/premiere.win1252.csv diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index e55471a7c4..e1b7e3a2f1 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -458,6 +458,24 @@ $reader->setSheetIndex(0); $spreadsheet = $reader->load("sample.csv"); ``` +You may also let PhpSpreadsheet attempt to guess the input encoding. +It will do so based on a test for BOM (UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, +or UTF-32LE), +or by doing heuristic tests for those encodings, falling back to a +specifiable encoding (default is CP1252) if all of those tests fail. + +```php +$reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); +$encoding = \PhpOffice\PhpSpreadsheet\Reader\Csv::guessEncoding('sample.csv'); +// or, e.g. $encoding = \PhpOffice\PhpSpreadsheet\Reader\Csv::guessEncoding( +// 'sample.csv', 'ISO-8859-2'); +$reader->setInputEncoding($encoding); +$reader->setDelimiter(';'); +$reader->setEnclosure(''); +$reader->setSheetIndex(0); + +$spreadsheet = $reader->load('sample.csv'); +``` #### Read a specific worksheet diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index d6eb16b0af..1495d102c0 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -9,6 +9,21 @@ class Csv extends BaseReader { + const UTF8_BOM = "\xEF\xBB\xBF"; + const UTF8_BOM_LEN = 3; + const UTF16BE_BOM = "\xfe\xff"; + const UTF16BE_BOM_LEN = 2; + const UTF16BE_LF = "\x00\x0a"; + const UTF16LE_BOM = "\xff\xfe"; + const UTF16LE_BOM_LEN = 2; + const UTF16LE_LF = "\x0a\x00"; + const UTF32BE_BOM = "\x00\x00\xfe\xff"; + const UTF32BE_BOM_LEN = 4; + const UTF32BE_LF = "\x00\x00\x00\x0a"; + const UTF32LE_BOM = "\xff\xfe\x00\x00"; + const UTF32LE_BOM_LEN = 4; + const UTF32LE_LF = "\x0a\x00\x00\x00"; + /** * Input encoding. * @@ -90,12 +105,8 @@ protected function skipBOM(): void { rewind($this->fileHandle); - switch ($this->inputEncoding) { - case 'UTF-8': - fgets($this->fileHandle, 4) == "\xEF\xBB\xBF" ? - fseek($this->fileHandle, 3) : fseek($this->fileHandle, 0); - - break; + if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) { + rewind($this->fileHandle); } } @@ -213,7 +224,9 @@ function ($sum, $value) use ($median) { private function getNextLine() { $line = ''; - $enclosure = '(?escapeCharacter, '/') . ')' . preg_quote($this->enclosure, '/'); + $enclosure = ($this->escapeCharacter === '' ? '' + : ('(?escapeCharacter, '/') . ')')) + . preg_quote($this->enclosure, '/'); do { // Get the next line in the file @@ -307,7 +320,7 @@ private function openFileOrMemory($pFilename): void $this->fileHandle = fopen('php://memory', 'r+b'); $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding); fwrite($this->fileHandle, $data); - rewind($this->fileHandle); + $this->skipBOM(); } } @@ -531,4 +544,63 @@ public function canRead($pFilename) return in_array($type, $supportedTypes, true); } + + private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void + { + if ($encoding === '') { + $pos = strpos($contents, $compare); + if ($pos !== false && $pos % strlen($compare) === 0) { + $encoding = $setEncoding; + } + } + } + + private static function guessEncodingNoBom(string $filename): string + { + $encoding = ''; + $contents = file_get_contents($filename); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE'); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE'); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE'); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE'); + if ($encoding === '' && preg_match('//u', $contents) === 1) { + $encoding = 'UTF-8'; + } + + return $encoding; + } + + private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void + { + if ($encoding === '') { + if ($compare === substr($first4, 0, strlen($compare))) { + $encoding = $setEncoding; + } + } + } + + private static function guessEncodingBom(string $filename): string + { + $encoding = ''; + $first4 = file_get_contents($filename, false, null, 0, 4); + if ($first4 !== false) { + self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); + } + + return $encoding; + } + + public static function guessEncoding(string $filename, string $dflt = 'CP1252'): string + { + $encoding = self::guessEncodingBom($filename); + if ($encoding === '') { + $encoding = self::guessEncodingNoBom($filename); + } + + return ($encoding === '') ? $dflt : $encoding; + } } diff --git a/tests/PhpSpreadsheetTests/Reader/CsvTest.php b/tests/PhpSpreadsheetTests/Reader/CsvTest.php index 797f3f1deb..e543ff4832 100644 --- a/tests/PhpSpreadsheetTests/Reader/CsvTest.php +++ b/tests/PhpSpreadsheetTests/Reader/CsvTest.php @@ -275,4 +275,66 @@ public function testReadNonexistentFileName(): void $reader = new Csv(); $reader->load('tests/data/Reader/CSV/encoding.utf8.csvxxx'); } + + /** + * @dataProvider providerEscapes + */ + public function testInferSeparator(string $escape, string $delimiter): void + { + $reader = new Csv(); + $reader->setEscapeCharacter($escape); + $filename = 'tests/data/Reader/CSV/escape.csv'; + $reader->listWorksheetInfo($filename); + self::assertEquals($delimiter, $reader->getDelimiter()); + } + + public function providerEscapes() + { + return [ + ['\\', ';'], + ["\x0", ','], + [(version_compare(PHP_VERSION, '7.4') < 0) ? "\x0" : '', ','], + ]; + } + + /** + * @dataProvider providerGuessEncoding + */ + public function testGuessEncoding(string $filename): void + { + $reader = new Csv(); + $reader->setInputEncoding(Csv::guessEncoding($filename)); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('première', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); + } + + public function providerGuessEncoding() + { + return [ + ['tests/data/Reader/CSV/premiere.utf8.csv'], + ['tests/data/Reader/CSV/premiere.utf8bom.csv'], + ['tests/data/Reader/CSV/premiere.utf16be.csv'], + ['tests/data/Reader/CSV/premiere.utf16bebom.csv'], + ['tests/data/Reader/CSV/premiere.utf16le.csv'], + ['tests/data/Reader/CSV/premiere.utf16lebom.csv'], + ['tests/data/Reader/CSV/premiere.utf32be.csv'], + ['tests/data/Reader/CSV/premiere.utf32bebom.csv'], + ['tests/data/Reader/CSV/premiere.utf32le.csv'], + ['tests/data/Reader/CSV/premiere.utf32lebom.csv'], + ['tests/data/Reader/CSV/premiere.win1252.csv'], + ]; + } + + public function testGuessEncodingDefltIso2(): void + { + $filename = 'tests/data/Reader/CSV/premiere.win1252.csv'; + $reader = new Csv(); + $reader->setInputEncoding(Csv::guessEncoding($filename, 'ISO-8859-2')); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('premičre', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixičme', $sheet->getCell('C2')->getValue()); + } } diff --git a/tests/data/Reader/CSV/escape.csv b/tests/data/Reader/CSV/escape.csv new file mode 100644 index 0000000000..a8b0c08435 --- /dev/null +++ b/tests/data/Reader/CSV/escape.csv @@ -0,0 +1,4 @@ +a\"hello;hello;hello;\",b\"hello;hello;hello;\",c\"\hello;hello;hello;\" +a\"hello;hello;hello;\",b\"hello;hello;hello;\",c\"\hello;hello;hello;\",d +a\"hello;hello;hello;\",b\"hello;hello;hello;\",c\"\hello;hello;hello;\" +a\"hello;hello;hello;\",b\"hello;hello;hello;\",c\"\hello;hello;hello;\" diff --git a/tests/data/Reader/CSV/premiere.utf16be.csv b/tests/data/Reader/CSV/premiere.utf16be.csv new file mode 100644 index 0000000000000000000000000000000000000000..44c25684bc93576b6b0eac52da1170f3170f3ae6 GIT binary patch literal 112 zcmYL=u?>JQ3y4PCOUIC@0^;Nq~?_QnOFC>RlYSx7j*yt literal 0 HcmV?d00001 diff --git a/tests/data/Reader/CSV/premiere.utf16bebom.csv b/tests/data/Reader/CSV/premiere.utf16bebom.csv new file mode 100644 index 0000000000000000000000000000000000000000..2d63bbe12f6204cac31e9757141c625e75e47727 GIT binary patch literal 114 zcmYL=K@vbf3O-)j>%KXf$^V=$47Xuk` literal 0 HcmV?d00001 diff --git a/tests/data/Reader/CSV/premiere.utf16le.csv b/tests/data/Reader/CSV/premiere.utf16le.csv new file mode 100644 index 0000000000000000000000000000000000000000..a5bb1ff12e771e8628bf3c52930311bffbb3ca94 GIT binary patch literal 112 zcmYL=u?>JQ3`X59ggzQuxx_;(w<^xk27j*yt literal 0 HcmV?d00001 diff --git a/tests/data/Reader/CSV/premiere.utf32bebom.csv b/tests/data/Reader/CSV/premiere.utf32bebom.csv new file mode 100644 index 0000000000000000000000000000000000000000..83326b64e44768f5b574f8c70f3af8745838be05 GIT binary patch literal 228 zcmZwAxe0(!5Jl061v{}BOAs|Ma96Not5EM1ej)}3hS^@kbw|X61uc4XIFWh<^$O=T sMhs}+Y=gV@y>&PJ@Sk@%GL+a+A>Dm<{b`PUb7pw+x0kYxpZS~l027xPasU7T literal 0 HcmV?d00001 diff --git a/tests/data/Reader/CSV/premiere.utf32le.csv b/tests/data/Reader/CSV/premiere.utf32le.csv new file mode 100644 index 0000000000000000000000000000000000000000..64d29f13cfadcbf775041cafc2b5177f8dcad164 GIT binary patch literal 224 zcmZwAxe0(!5Jl061v{}BOAs|Ma96No%kbV{68XV_VYatL#DNhrCcMaALw&+|j};3B oIJ@AkeQ({(AO7>MMuiqPI^^Ad*PrItH)n@8rM=903iHhT0GW0dbpQYW literal 0 HcmV?d00001 diff --git a/tests/data/Reader/CSV/premiere.utf32lebom.csv b/tests/data/Reader/CSV/premiere.utf32lebom.csv new file mode 100644 index 0000000000000000000000000000000000000000..25617c6e9b2a51e78b00ac8e376509cb8c45f0e8 GIT binary patch literal 228 zcmZwAy9tC~5JlmMh3&*)uzyFCrH77%|{P_G;=C t&cB#3p@Xvx?%Ma(-TdJ{?>?x|U`LC*d+++w9Q)?{;Z12TvyQ?%Gap^<8FBys literal 0 HcmV?d00001 diff --git a/tests/data/Reader/CSV/premiere.utf8.csv b/tests/data/Reader/CSV/premiere.utf8.csv new file mode 100644 index 0000000000..c668120175 --- /dev/null +++ b/tests/data/Reader/CSV/premiere.utf8.csv @@ -0,0 +1,2 @@ +première,second,troisième +Quatrième,cinquième,sixième diff --git a/tests/data/Reader/CSV/premiere.utf8bom.csv b/tests/data/Reader/CSV/premiere.utf8bom.csv new file mode 100644 index 0000000000..4068e6c38e --- /dev/null +++ b/tests/data/Reader/CSV/premiere.utf8bom.csv @@ -0,0 +1,2 @@ +première,second,troisième +Quatrième,cinquième,sixième diff --git a/tests/data/Reader/CSV/premiere.win1252.csv b/tests/data/Reader/CSV/premiere.win1252.csv new file mode 100644 index 0000000000..908cb88fe2 --- /dev/null +++ b/tests/data/Reader/CSV/premiere.win1252.csv @@ -0,0 +1,2 @@ +premire,second,troisime +Quatrime,cinquime,sixime From a46032b73586c1ee78734a9c87345e49c9bceabf Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 25 Dec 2020 17:54:58 +0100 Subject: [PATCH 32/38] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fdd7180a..23e7b13a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Nothing. +- CSV Reader - Best Guess for Encoding, and Handle Null-string Escape [#1647](https://github.com/PHPOffice/PhpSpreadsheet/issues/1647) ### Changed From bad864791fbe18bfc1cc56a4821bf1dbf1f566a1 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 29 Dec 2020 18:19:43 +0100 Subject: [PATCH 33/38] Update Units of Measure supported by the CONVERT() function (#1768) Now supports all current UoM in all categories, with both 1- and 2-character multiplier prefixes, and binary multiplier prefixes, including the new Temperature scales --- CHANGELOG.md | 2 +- .../Calculation/Engineering.php | 859 +----------------- .../Calculation/Engineering/ConvertUOM.php | 684 ++++++++++++++ .../Calculation/Engineering/CONVERTUOM.php | 200 ++-- 4 files changed, 856 insertions(+), 889 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e7b13a04..b8e37e669d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed -- Nothing. +- Updated the CONVERT() function to support all current MS Excel categories and Units of Measure. ### Deprecated diff --git a/src/PhpSpreadsheet/Calculation/Engineering.php b/src/PhpSpreadsheet/Calculation/Engineering.php index 1256dd90be..57116f28f5 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering.php +++ b/src/PhpSpreadsheet/Calculation/Engineering.php @@ -4,6 +4,7 @@ use Complex\Complex; use Complex\Exception as ComplexException; +use PhpOffice\PhpSpreadsheet\Calculation\Engineering\ConvertUOM; class Engineering { @@ -12,710 +13,6 @@ class Engineering */ const EULER = 2.71828182845904523536; - /** - * Details of the Units of measure that can be used in CONVERTUOM(). - * - * @var mixed[] - */ - private static $conversionUnits = [ - 'g' => ['Group' => 'Mass', 'Unit Name' => 'Gram', 'AllowPrefix' => true], - 'sg' => ['Group' => 'Mass', 'Unit Name' => 'Slug', 'AllowPrefix' => false], - 'lbm' => ['Group' => 'Mass', 'Unit Name' => 'Pound mass (avoirdupois)', 'AllowPrefix' => false], - 'u' => ['Group' => 'Mass', 'Unit Name' => 'U (atomic mass unit)', 'AllowPrefix' => true], - 'ozm' => ['Group' => 'Mass', 'Unit Name' => 'Ounce mass (avoirdupois)', 'AllowPrefix' => false], - 'm' => ['Group' => 'Distance', 'Unit Name' => 'Meter', 'AllowPrefix' => true], - 'mi' => ['Group' => 'Distance', 'Unit Name' => 'Statute mile', 'AllowPrefix' => false], - 'Nmi' => ['Group' => 'Distance', 'Unit Name' => 'Nautical mile', 'AllowPrefix' => false], - 'in' => ['Group' => 'Distance', 'Unit Name' => 'Inch', 'AllowPrefix' => false], - 'ft' => ['Group' => 'Distance', 'Unit Name' => 'Foot', 'AllowPrefix' => false], - 'yd' => ['Group' => 'Distance', 'Unit Name' => 'Yard', 'AllowPrefix' => false], - 'ang' => ['Group' => 'Distance', 'Unit Name' => 'Angstrom', 'AllowPrefix' => true], - 'Pica' => ['Group' => 'Distance', 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false], - 'yr' => ['Group' => 'Time', 'Unit Name' => 'Year', 'AllowPrefix' => false], - 'day' => ['Group' => 'Time', 'Unit Name' => 'Day', 'AllowPrefix' => false], - 'hr' => ['Group' => 'Time', 'Unit Name' => 'Hour', 'AllowPrefix' => false], - 'mn' => ['Group' => 'Time', 'Unit Name' => 'Minute', 'AllowPrefix' => false], - 'sec' => ['Group' => 'Time', 'Unit Name' => 'Second', 'AllowPrefix' => true], - 'Pa' => ['Group' => 'Pressure', 'Unit Name' => 'Pascal', 'AllowPrefix' => true], - 'p' => ['Group' => 'Pressure', 'Unit Name' => 'Pascal', 'AllowPrefix' => true], - 'atm' => ['Group' => 'Pressure', 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true], - 'at' => ['Group' => 'Pressure', 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true], - 'mmHg' => ['Group' => 'Pressure', 'Unit Name' => 'mm of Mercury', 'AllowPrefix' => true], - 'N' => ['Group' => 'Force', 'Unit Name' => 'Newton', 'AllowPrefix' => true], - 'dyn' => ['Group' => 'Force', 'Unit Name' => 'Dyne', 'AllowPrefix' => true], - 'dy' => ['Group' => 'Force', 'Unit Name' => 'Dyne', 'AllowPrefix' => true], - 'lbf' => ['Group' => 'Force', 'Unit Name' => 'Pound force', 'AllowPrefix' => false], - 'J' => ['Group' => 'Energy', 'Unit Name' => 'Joule', 'AllowPrefix' => true], - 'e' => ['Group' => 'Energy', 'Unit Name' => 'Erg', 'AllowPrefix' => true], - 'c' => ['Group' => 'Energy', 'Unit Name' => 'Thermodynamic calorie', 'AllowPrefix' => true], - 'cal' => ['Group' => 'Energy', 'Unit Name' => 'IT calorie', 'AllowPrefix' => true], - 'eV' => ['Group' => 'Energy', 'Unit Name' => 'Electron volt', 'AllowPrefix' => true], - 'ev' => ['Group' => 'Energy', 'Unit Name' => 'Electron volt', 'AllowPrefix' => true], - 'HPh' => ['Group' => 'Energy', 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false], - 'hh' => ['Group' => 'Energy', 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false], - 'Wh' => ['Group' => 'Energy', 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true], - 'wh' => ['Group' => 'Energy', 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true], - 'flb' => ['Group' => 'Energy', 'Unit Name' => 'Foot-pound', 'AllowPrefix' => false], - 'BTU' => ['Group' => 'Energy', 'Unit Name' => 'BTU', 'AllowPrefix' => false], - 'btu' => ['Group' => 'Energy', 'Unit Name' => 'BTU', 'AllowPrefix' => false], - 'HP' => ['Group' => 'Power', 'Unit Name' => 'Horsepower', 'AllowPrefix' => false], - 'h' => ['Group' => 'Power', 'Unit Name' => 'Horsepower', 'AllowPrefix' => false], - 'W' => ['Group' => 'Power', 'Unit Name' => 'Watt', 'AllowPrefix' => true], - 'w' => ['Group' => 'Power', 'Unit Name' => 'Watt', 'AllowPrefix' => true], - 'T' => ['Group' => 'Magnetism', 'Unit Name' => 'Tesla', 'AllowPrefix' => true], - 'ga' => ['Group' => 'Magnetism', 'Unit Name' => 'Gauss', 'AllowPrefix' => true], - 'C' => ['Group' => 'Temperature', 'Unit Name' => 'Celsius', 'AllowPrefix' => false], - 'cel' => ['Group' => 'Temperature', 'Unit Name' => 'Celsius', 'AllowPrefix' => false], - 'F' => ['Group' => 'Temperature', 'Unit Name' => 'Fahrenheit', 'AllowPrefix' => false], - 'fah' => ['Group' => 'Temperature', 'Unit Name' => 'Fahrenheit', 'AllowPrefix' => false], - 'K' => ['Group' => 'Temperature', 'Unit Name' => 'Kelvin', 'AllowPrefix' => false], - 'kel' => ['Group' => 'Temperature', 'Unit Name' => 'Kelvin', 'AllowPrefix' => false], - 'tsp' => ['Group' => 'Liquid', 'Unit Name' => 'Teaspoon', 'AllowPrefix' => false], - 'tbs' => ['Group' => 'Liquid', 'Unit Name' => 'Tablespoon', 'AllowPrefix' => false], - 'oz' => ['Group' => 'Liquid', 'Unit Name' => 'Fluid Ounce', 'AllowPrefix' => false], - 'cup' => ['Group' => 'Liquid', 'Unit Name' => 'Cup', 'AllowPrefix' => false], - 'pt' => ['Group' => 'Liquid', 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false], - 'us_pt' => ['Group' => 'Liquid', 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false], - 'uk_pt' => ['Group' => 'Liquid', 'Unit Name' => 'U.K. Pint', 'AllowPrefix' => false], - 'qt' => ['Group' => 'Liquid', 'Unit Name' => 'Quart', 'AllowPrefix' => false], - 'gal' => ['Group' => 'Liquid', 'Unit Name' => 'Gallon', 'AllowPrefix' => false], - 'l' => ['Group' => 'Liquid', 'Unit Name' => 'Litre', 'AllowPrefix' => true], - 'lt' => ['Group' => 'Liquid', 'Unit Name' => 'Litre', 'AllowPrefix' => true], - ]; - - /** - * Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). - * - * @var mixed[] - */ - private static $conversionMultipliers = [ - 'Y' => ['multiplier' => 1E24, 'name' => 'yotta'], - 'Z' => ['multiplier' => 1E21, 'name' => 'zetta'], - 'E' => ['multiplier' => 1E18, 'name' => 'exa'], - 'P' => ['multiplier' => 1E15, 'name' => 'peta'], - 'T' => ['multiplier' => 1E12, 'name' => 'tera'], - 'G' => ['multiplier' => 1E9, 'name' => 'giga'], - 'M' => ['multiplier' => 1E6, 'name' => 'mega'], - 'k' => ['multiplier' => 1E3, 'name' => 'kilo'], - 'h' => ['multiplier' => 1E2, 'name' => 'hecto'], - 'e' => ['multiplier' => 1E1, 'name' => 'deka'], - 'd' => ['multiplier' => 1E-1, 'name' => 'deci'], - 'c' => ['multiplier' => 1E-2, 'name' => 'centi'], - 'm' => ['multiplier' => 1E-3, 'name' => 'milli'], - 'u' => ['multiplier' => 1E-6, 'name' => 'micro'], - 'n' => ['multiplier' => 1E-9, 'name' => 'nano'], - 'p' => ['multiplier' => 1E-12, 'name' => 'pico'], - 'f' => ['multiplier' => 1E-15, 'name' => 'femto'], - 'a' => ['multiplier' => 1E-18, 'name' => 'atto'], - 'z' => ['multiplier' => 1E-21, 'name' => 'zepto'], - 'y' => ['multiplier' => 1E-24, 'name' => 'yocto'], - ]; - - /** - * Details of the Units of measure conversion factors, organised by group. - * - * @var mixed[] - */ - private static $unitConversions = [ - 'Mass' => [ - 'g' => [ - 'g' => 1.0, - 'sg' => 6.85220500053478E-05, - 'lbm' => 2.20462291469134E-03, - 'u' => 6.02217000000000E+23, - 'ozm' => 3.52739718003627E-02, - ], - 'sg' => [ - 'g' => 1.45938424189287E+04, - 'sg' => 1.0, - 'lbm' => 3.21739194101647E+01, - 'u' => 8.78866000000000E+27, - 'ozm' => 5.14782785944229E+02, - ], - 'lbm' => [ - 'g' => 4.5359230974881148E+02, - 'sg' => 3.10810749306493E-02, - 'lbm' => 1.0, - 'u' => 2.73161000000000E+26, - 'ozm' => 1.60000023429410E+01, - ], - 'u' => [ - 'g' => 1.66053100460465E-24, - 'sg' => 1.13782988532950E-28, - 'lbm' => 3.66084470330684E-27, - 'u' => 1.0, - 'ozm' => 5.85735238300524E-26, - ], - 'ozm' => [ - 'g' => 2.83495152079732E+01, - 'sg' => 1.94256689870811E-03, - 'lbm' => 6.24999908478882E-02, - 'u' => 1.70725600000000E+25, - 'ozm' => 1.0, - ], - ], - 'Distance' => [ - 'm' => [ - 'm' => 1.0, - 'mi' => 6.21371192237334E-04, - 'Nmi' => 5.39956803455724E-04, - 'in' => 3.93700787401575E+01, - 'ft' => 3.28083989501312E+00, - 'yd' => 1.09361329797891E+00, - 'ang' => 1.00000000000000E+10, - 'Pica' => 2.83464566929116E+03, - ], - 'mi' => [ - 'm' => 1.60934400000000E+03, - 'mi' => 1.0, - 'Nmi' => 8.68976241900648E-01, - 'in' => 6.33600000000000E+04, - 'ft' => 5.28000000000000E+03, - 'yd' => 1.76000000000000E+03, - 'ang' => 1.60934400000000E+13, - 'Pica' => 4.56191999999971E+06, - ], - 'Nmi' => [ - 'm' => 1.85200000000000E+03, - 'mi' => 1.15077944802354E+00, - 'Nmi' => 1.0, - 'in' => 7.29133858267717E+04, - 'ft' => 6.07611548556430E+03, - 'yd' => 2.02537182785694E+03, - 'ang' => 1.85200000000000E+13, - 'Pica' => 5.24976377952723E+06, - ], - 'in' => [ - 'm' => 2.54000000000000E-02, - 'mi' => 1.57828282828283E-05, - 'Nmi' => 1.37149028077754E-05, - 'in' => 1.0, - 'ft' => 8.33333333333333E-02, - 'yd' => 2.77777777686643E-02, - 'ang' => 2.54000000000000E+08, - 'Pica' => 7.19999999999955E+01, - ], - 'ft' => [ - 'm' => 3.04800000000000E-01, - 'mi' => 1.89393939393939E-04, - 'Nmi' => 1.64578833693305E-04, - 'in' => 1.20000000000000E+01, - 'ft' => 1.0, - 'yd' => 3.33333333223972E-01, - 'ang' => 3.04800000000000E+09, - 'Pica' => 8.63999999999946E+02, - ], - 'yd' => [ - 'm' => 9.14400000300000E-01, - 'mi' => 5.68181818368230E-04, - 'Nmi' => 4.93736501241901E-04, - 'in' => 3.60000000118110E+01, - 'ft' => 3.00000000000000E+00, - 'yd' => 1.0, - 'ang' => 9.14400000300000E+09, - 'Pica' => 2.59200000085023E+03, - ], - 'ang' => [ - 'm' => 1.00000000000000E-10, - 'mi' => 6.21371192237334E-14, - 'Nmi' => 5.39956803455724E-14, - 'in' => 3.93700787401575E-09, - 'ft' => 3.28083989501312E-10, - 'yd' => 1.09361329797891E-10, - 'ang' => 1.0, - 'Pica' => 2.83464566929116E-07, - ], - 'Pica' => [ - 'm' => 3.52777777777800E-04, - 'mi' => 2.19205948372629E-07, - 'Nmi' => 1.90484761219114E-07, - 'in' => 1.38888888888898E-02, - 'ft' => 1.15740740740748E-03, - 'yd' => 3.85802469009251E-04, - 'ang' => 3.52777777777800E+06, - 'Pica' => 1.0, - ], - ], - 'Time' => [ - 'yr' => [ - 'yr' => 1.0, - 'day' => 365.25, - 'hr' => 8766.0, - 'mn' => 525960.0, - 'sec' => 31557600.0, - ], - 'day' => [ - 'yr' => 2.73785078713210E-03, - 'day' => 1.0, - 'hr' => 24.0, - 'mn' => 1440.0, - 'sec' => 86400.0, - ], - 'hr' => [ - 'yr' => 1.14077116130504E-04, - 'day' => 4.16666666666667E-02, - 'hr' => 1.0, - 'mn' => 60.0, - 'sec' => 3600.0, - ], - 'mn' => [ - 'yr' => 1.90128526884174E-06, - 'day' => 6.94444444444444E-04, - 'hr' => 1.66666666666667E-02, - 'mn' => 1.0, - 'sec' => 60.0, - ], - 'sec' => [ - 'yr' => 3.16880878140289E-08, - 'day' => 1.15740740740741E-05, - 'hr' => 2.77777777777778E-04, - 'mn' => 1.66666666666667E-02, - 'sec' => 1.0, - ], - ], - 'Pressure' => [ - 'Pa' => [ - 'Pa' => 1.0, - 'p' => 1.0, - 'atm' => 9.86923299998193E-06, - 'at' => 9.86923299998193E-06, - 'mmHg' => 7.50061707998627E-03, - ], - 'p' => [ - 'Pa' => 1.0, - 'p' => 1.0, - 'atm' => 9.86923299998193E-06, - 'at' => 9.86923299998193E-06, - 'mmHg' => 7.50061707998627E-03, - ], - 'atm' => [ - 'Pa' => 1.01324996583000E+05, - 'p' => 1.01324996583000E+05, - 'atm' => 1.0, - 'at' => 1.0, - 'mmHg' => 760.0, - ], - 'at' => [ - 'Pa' => 1.01324996583000E+05, - 'p' => 1.01324996583000E+05, - 'atm' => 1.0, - 'at' => 1.0, - 'mmHg' => 760.0, - ], - 'mmHg' => [ - 'Pa' => 1.33322363925000E+02, - 'p' => 1.33322363925000E+02, - 'atm' => 1.31578947368421E-03, - 'at' => 1.31578947368421E-03, - 'mmHg' => 1.0, - ], - ], - 'Force' => [ - 'N' => [ - 'N' => 1.0, - 'dyn' => 1.0E+5, - 'dy' => 1.0E+5, - 'lbf' => 2.24808923655339E-01, - ], - 'dyn' => [ - 'N' => 1.0E-5, - 'dyn' => 1.0, - 'dy' => 1.0, - 'lbf' => 2.24808923655339E-06, - ], - 'dy' => [ - 'N' => 1.0E-5, - 'dyn' => 1.0, - 'dy' => 1.0, - 'lbf' => 2.24808923655339E-06, - ], - 'lbf' => [ - 'N' => 4.448222, - 'dyn' => 4.448222E+5, - 'dy' => 4.448222E+5, - 'lbf' => 1.0, - ], - ], - 'Energy' => [ - 'J' => [ - 'J' => 1.0, - 'e' => 9.99999519343231E+06, - 'c' => 2.39006249473467E-01, - 'cal' => 2.38846190642017E-01, - 'eV' => 6.24145700000000E+18, - 'ev' => 6.24145700000000E+18, - 'HPh' => 3.72506430801000E-07, - 'hh' => 3.72506430801000E-07, - 'Wh' => 2.77777916238711E-04, - 'wh' => 2.77777916238711E-04, - 'flb' => 2.37304222192651E+01, - 'BTU' => 9.47815067349015E-04, - 'btu' => 9.47815067349015E-04, - ], - 'e' => [ - 'J' => 1.00000048065700E-07, - 'e' => 1.0, - 'c' => 2.39006364353494E-08, - 'cal' => 2.38846305445111E-08, - 'eV' => 6.24146000000000E+11, - 'ev' => 6.24146000000000E+11, - 'HPh' => 3.72506609848824E-14, - 'hh' => 3.72506609848824E-14, - 'Wh' => 2.77778049754611E-11, - 'wh' => 2.77778049754611E-11, - 'flb' => 2.37304336254586E-06, - 'BTU' => 9.47815522922962E-11, - 'btu' => 9.47815522922962E-11, - ], - 'c' => [ - 'J' => 4.18399101363672E+00, - 'e' => 4.18398900257312E+07, - 'c' => 1.0, - 'cal' => 9.99330315287563E-01, - 'eV' => 2.61142000000000E+19, - 'ev' => 2.61142000000000E+19, - 'HPh' => 1.55856355899327E-06, - 'hh' => 1.55856355899327E-06, - 'Wh' => 1.16222030532950E-03, - 'wh' => 1.16222030532950E-03, - 'flb' => 9.92878733152102E+01, - 'BTU' => 3.96564972437776E-03, - 'btu' => 3.96564972437776E-03, - ], - 'cal' => [ - 'J' => 4.18679484613929E+00, - 'e' => 4.18679283372801E+07, - 'c' => 1.00067013349059E+00, - 'cal' => 1.0, - 'eV' => 2.61317000000000E+19, - 'ev' => 2.61317000000000E+19, - 'HPh' => 1.55960800463137E-06, - 'hh' => 1.55960800463137E-06, - 'Wh' => 1.16299914807955E-03, - 'wh' => 1.16299914807955E-03, - 'flb' => 9.93544094443283E+01, - 'BTU' => 3.96830723907002E-03, - 'btu' => 3.96830723907002E-03, - ], - 'eV' => [ - 'J' => 1.60219000146921E-19, - 'e' => 1.60218923136574E-12, - 'c' => 3.82933423195043E-20, - 'cal' => 3.82676978535648E-20, - 'eV' => 1.0, - 'ev' => 1.0, - 'HPh' => 5.96826078912344E-26, - 'hh' => 5.96826078912344E-26, - 'Wh' => 4.45053000026614E-23, - 'wh' => 4.45053000026614E-23, - 'flb' => 3.80206452103492E-18, - 'BTU' => 1.51857982414846E-22, - 'btu' => 1.51857982414846E-22, - ], - 'ev' => [ - 'J' => 1.60219000146921E-19, - 'e' => 1.60218923136574E-12, - 'c' => 3.82933423195043E-20, - 'cal' => 3.82676978535648E-20, - 'eV' => 1.0, - 'ev' => 1.0, - 'HPh' => 5.96826078912344E-26, - 'hh' => 5.96826078912344E-26, - 'Wh' => 4.45053000026614E-23, - 'wh' => 4.45053000026614E-23, - 'flb' => 3.80206452103492E-18, - 'BTU' => 1.51857982414846E-22, - 'btu' => 1.51857982414846E-22, - ], - 'HPh' => [ - 'J' => 2.68451741316170E+06, - 'e' => 2.68451612283024E+13, - 'c' => 6.41616438565991E+05, - 'cal' => 6.41186757845835E+05, - 'eV' => 1.67553000000000E+25, - 'ev' => 1.67553000000000E+25, - 'HPh' => 1.0, - 'hh' => 1.0, - 'Wh' => 7.45699653134593E+02, - 'wh' => 7.45699653134593E+02, - 'flb' => 6.37047316692964E+07, - 'BTU' => 2.54442605275546E+03, - 'btu' => 2.54442605275546E+03, - ], - 'hh' => [ - 'J' => 2.68451741316170E+06, - 'e' => 2.68451612283024E+13, - 'c' => 6.41616438565991E+05, - 'cal' => 6.41186757845835E+05, - 'eV' => 1.67553000000000E+25, - 'ev' => 1.67553000000000E+25, - 'HPh' => 1.0, - 'hh' => 1.0, - 'Wh' => 7.45699653134593E+02, - 'wh' => 7.45699653134593E+02, - 'flb' => 6.37047316692964E+07, - 'BTU' => 2.54442605275546E+03, - 'btu' => 2.54442605275546E+03, - ], - 'Wh' => [ - 'J' => 3.59999820554720E+03, - 'e' => 3.59999647518369E+10, - 'c' => 8.60422069219046E+02, - 'cal' => 8.59845857713046E+02, - 'eV' => 2.24692340000000E+22, - 'ev' => 2.24692340000000E+22, - 'HPh' => 1.34102248243839E-03, - 'hh' => 1.34102248243839E-03, - 'Wh' => 1.0, - 'wh' => 1.0, - 'flb' => 8.54294774062316E+04, - 'BTU' => 3.41213254164705E+00, - 'btu' => 3.41213254164705E+00, - ], - 'wh' => [ - 'J' => 3.59999820554720E+03, - 'e' => 3.59999647518369E+10, - 'c' => 8.60422069219046E+02, - 'cal' => 8.59845857713046E+02, - 'eV' => 2.24692340000000E+22, - 'ev' => 2.24692340000000E+22, - 'HPh' => 1.34102248243839E-03, - 'hh' => 1.34102248243839E-03, - 'Wh' => 1.0, - 'wh' => 1.0, - 'flb' => 8.54294774062316E+04, - 'BTU' => 3.41213254164705E+00, - 'btu' => 3.41213254164705E+00, - ], - 'flb' => [ - 'J' => 4.21400003236424E-02, - 'e' => 4.21399800687660E+05, - 'c' => 1.00717234301644E-02, - 'cal' => 1.00649785509554E-02, - 'eV' => 2.63015000000000E+17, - 'ev' => 2.63015000000000E+17, - 'HPh' => 1.56974211145130E-08, - 'hh' => 1.56974211145130E-08, - 'Wh' => 1.17055614802000E-05, - 'wh' => 1.17055614802000E-05, - 'flb' => 1.0, - 'BTU' => 3.99409272448406E-05, - 'btu' => 3.99409272448406E-05, - ], - 'BTU' => [ - 'J' => 1.05505813786749E+03, - 'e' => 1.05505763074665E+10, - 'c' => 2.52165488508168E+02, - 'cal' => 2.51996617135510E+02, - 'eV' => 6.58510000000000E+21, - 'ev' => 6.58510000000000E+21, - 'HPh' => 3.93015941224568E-04, - 'hh' => 3.93015941224568E-04, - 'Wh' => 2.93071851047526E-01, - 'wh' => 2.93071851047526E-01, - 'flb' => 2.50369750774671E+04, - 'BTU' => 1.0, - 'btu' => 1.0, - ], - 'btu' => [ - 'J' => 1.05505813786749E+03, - 'e' => 1.05505763074665E+10, - 'c' => 2.52165488508168E+02, - 'cal' => 2.51996617135510E+02, - 'eV' => 6.58510000000000E+21, - 'ev' => 6.58510000000000E+21, - 'HPh' => 3.93015941224568E-04, - 'hh' => 3.93015941224568E-04, - 'Wh' => 2.93071851047526E-01, - 'wh' => 2.93071851047526E-01, - 'flb' => 2.50369750774671E+04, - 'BTU' => 1.0, - 'btu' => 1.0, - ], - ], - 'Power' => [ - 'HP' => [ - 'HP' => 1.0, - 'h' => 1.0, - 'W' => 7.45701000000000E+02, - 'w' => 7.45701000000000E+02, - ], - 'h' => [ - 'HP' => 1.0, - 'h' => 1.0, - 'W' => 7.45701000000000E+02, - 'w' => 7.45701000000000E+02, - ], - 'W' => [ - 'HP' => 1.34102006031908E-03, - 'h' => 1.34102006031908E-03, - 'W' => 1.0, - 'w' => 1.0, - ], - 'w' => [ - 'HP' => 1.34102006031908E-03, - 'h' => 1.34102006031908E-03, - 'W' => 1.0, - 'w' => 1.0, - ], - ], - 'Magnetism' => [ - 'T' => [ - 'T' => 1.0, - 'ga' => 10000.0, - ], - 'ga' => [ - 'T' => 0.0001, - 'ga' => 1.0, - ], - ], - 'Liquid' => [ - 'tsp' => [ - 'tsp' => 1.0, - 'tbs' => 3.33333333333333E-01, - 'oz' => 1.66666666666667E-01, - 'cup' => 2.08333333333333E-02, - 'pt' => 1.04166666666667E-02, - 'us_pt' => 1.04166666666667E-02, - 'uk_pt' => 8.67558516821960E-03, - 'qt' => 5.20833333333333E-03, - 'gal' => 1.30208333333333E-03, - 'l' => 4.92999408400710E-03, - 'lt' => 4.92999408400710E-03, - ], - 'tbs' => [ - 'tsp' => 3.00000000000000E+00, - 'tbs' => 1.0, - 'oz' => 5.00000000000000E-01, - 'cup' => 6.25000000000000E-02, - 'pt' => 3.12500000000000E-02, - 'us_pt' => 3.12500000000000E-02, - 'uk_pt' => 2.60267555046588E-02, - 'qt' => 1.56250000000000E-02, - 'gal' => 3.90625000000000E-03, - 'l' => 1.47899822520213E-02, - 'lt' => 1.47899822520213E-02, - ], - 'oz' => [ - 'tsp' => 6.00000000000000E+00, - 'tbs' => 2.00000000000000E+00, - 'oz' => 1.0, - 'cup' => 1.25000000000000E-01, - 'pt' => 6.25000000000000E-02, - 'us_pt' => 6.25000000000000E-02, - 'uk_pt' => 5.20535110093176E-02, - 'qt' => 3.12500000000000E-02, - 'gal' => 7.81250000000000E-03, - 'l' => 2.95799645040426E-02, - 'lt' => 2.95799645040426E-02, - ], - 'cup' => [ - 'tsp' => 4.80000000000000E+01, - 'tbs' => 1.60000000000000E+01, - 'oz' => 8.00000000000000E+00, - 'cup' => 1.0, - 'pt' => 5.00000000000000E-01, - 'us_pt' => 5.00000000000000E-01, - 'uk_pt' => 4.16428088074541E-01, - 'qt' => 2.50000000000000E-01, - 'gal' => 6.25000000000000E-02, - 'l' => 2.36639716032341E-01, - 'lt' => 2.36639716032341E-01, - ], - 'pt' => [ - 'tsp' => 9.60000000000000E+01, - 'tbs' => 3.20000000000000E+01, - 'oz' => 1.60000000000000E+01, - 'cup' => 2.00000000000000E+00, - 'pt' => 1.0, - 'us_pt' => 1.0, - 'uk_pt' => 8.32856176149081E-01, - 'qt' => 5.00000000000000E-01, - 'gal' => 1.25000000000000E-01, - 'l' => 4.73279432064682E-01, - 'lt' => 4.73279432064682E-01, - ], - 'us_pt' => [ - 'tsp' => 9.60000000000000E+01, - 'tbs' => 3.20000000000000E+01, - 'oz' => 1.60000000000000E+01, - 'cup' => 2.00000000000000E+00, - 'pt' => 1.0, - 'us_pt' => 1.0, - 'uk_pt' => 8.32856176149081E-01, - 'qt' => 5.00000000000000E-01, - 'gal' => 1.25000000000000E-01, - 'l' => 4.73279432064682E-01, - 'lt' => 4.73279432064682E-01, - ], - 'uk_pt' => [ - 'tsp' => 1.15266000000000E+02, - 'tbs' => 3.84220000000000E+01, - 'oz' => 1.92110000000000E+01, - 'cup' => 2.40137500000000E+00, - 'pt' => 1.20068750000000E+00, - 'us_pt' => 1.20068750000000E+00, - 'uk_pt' => 1.0, - 'qt' => 6.00343750000000E-01, - 'gal' => 1.50085937500000E-01, - 'l' => 5.68260698087162E-01, - 'lt' => 5.68260698087162E-01, - ], - 'qt' => [ - 'tsp' => 1.92000000000000E+02, - 'tbs' => 6.40000000000000E+01, - 'oz' => 3.20000000000000E+01, - 'cup' => 4.00000000000000E+00, - 'pt' => 2.00000000000000E+00, - 'us_pt' => 2.00000000000000E+00, - 'uk_pt' => 1.66571235229816E+00, - 'qt' => 1.0, - 'gal' => 2.50000000000000E-01, - 'l' => 9.46558864129363E-01, - 'lt' => 9.46558864129363E-01, - ], - 'gal' => [ - 'tsp' => 7.68000000000000E+02, - 'tbs' => 2.56000000000000E+02, - 'oz' => 1.28000000000000E+02, - 'cup' => 1.60000000000000E+01, - 'pt' => 8.00000000000000E+00, - 'us_pt' => 8.00000000000000E+00, - 'uk_pt' => 6.66284940919265E+00, - 'qt' => 4.00000000000000E+00, - 'gal' => 1.0, - 'l' => 3.78623545651745E+00, - 'lt' => 3.78623545651745E+00, - ], - 'l' => [ - 'tsp' => 2.02840000000000E+02, - 'tbs' => 6.76133333333333E+01, - 'oz' => 3.38066666666667E+01, - 'cup' => 4.22583333333333E+00, - 'pt' => 2.11291666666667E+00, - 'us_pt' => 2.11291666666667E+00, - 'uk_pt' => 1.75975569552166E+00, - 'qt' => 1.05645833333333E+00, - 'gal' => 2.64114583333333E-01, - 'l' => 1.0, - 'lt' => 1.0, - ], - 'lt' => [ - 'tsp' => 2.02840000000000E+02, - 'tbs' => 6.76133333333333E+01, - 'oz' => 3.38066666666667E+01, - 'cup' => 4.22583333333333E+00, - 'pt' => 2.11291666666667E+00, - 'us_pt' => 2.11291666666667E+00, - 'uk_pt' => 1.75975569552166E+00, - 'qt' => 1.05645833333333E+00, - 'gal' => 2.64114583333333E-01, - 'l' => 1.0, - 'lt' => 1.0, - ], - ], - ]; - /** * parseComplex. * @@ -2585,69 +1882,68 @@ public static function ERFC($x) * getConversionGroups * Returns a list of the different conversion groups for UOM conversions. * + * @Deprecated Use the getConversionCategories() method in the ConvertUOM class instead + * * @return array */ public static function getConversionGroups() { - $conversionGroups = []; - foreach (self::$conversionUnits as $conversionUnit) { - $conversionGroups[] = $conversionUnit['Group']; - } - - return array_merge(array_unique($conversionGroups)); + return Engineering\ConvertUOM::getConversionCategories(); } /** * getConversionGroupUnits * Returns an array of units of measure, for a specified conversion group, or for all groups. * - * @param string $group The group whose units of measure you want to retrieve + * @Deprecated Use the getConversionCategoryUnits() method in the ConvertUOM class instead + * + * @param null|mixed $category * * @return array */ - public static function getConversionGroupUnits($group = null) + public static function getConversionGroupUnits($category = null) { - $conversionGroups = []; - foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { - if (($group === null) || ($conversionGroup['Group'] == $group)) { - $conversionGroups[$conversionGroup['Group']][] = $conversionUnit; - } - } - - return $conversionGroups; + return Engineering\ConvertUOM::getConversionCategoryUnits($category); } /** * getConversionGroupUnitDetails. * - * @param string $group The group whose units of measure you want to retrieve + * @Deprecated Use the getConversionCategoryUnitDetails() method in the ConvertUOM class instead + * + * @param null|mixed $category * * @return array */ - public static function getConversionGroupUnitDetails($group = null) - { - $conversionGroups = []; - foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { - if (($group === null) || ($conversionGroup['Group'] == $group)) { - $conversionGroups[$conversionGroup['Group']][] = [ - 'unit' => $conversionUnit, - 'description' => $conversionGroup['Unit Name'], - ]; - } - } - - return $conversionGroups; + public static function getConversionGroupUnitDetails($category = null) + { + return Engineering\ConvertUOM::getConversionCategoryUnitDetails($category); } /** * getConversionMultipliers * Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). * + * @Deprecated Use the getConversionMultipliers() method in the ConvertUOM class instead + * * @return array of mixed */ public static function getConversionMultipliers() { - return self::$conversionMultipliers; + return Engineering\ConvertUOM::getConversionMultipliers(); + } + + /** + * getBinaryConversionMultipliers + * Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure in CONVERTUOM(). + * + * @Deprecated Use the getBinaryConversionMultipliers() method in the ConvertUOM class instead + * + * @return array of mixed + */ + public static function getBinaryConversionMultipliers() + { + return Engineering\ConvertUOM::getBinaryConversionMultipliers(); } /** @@ -2660,7 +1956,9 @@ public static function getConversionMultipliers() * Excel Function: * CONVERT(value,fromUOM,toUOM) * - * @param float $value the value in fromUOM to convert + * @Deprecated Use the CONVERT() method in the ConvertUOM class instead + * + * @param float|int $value the value in fromUOM to convert * @param string $fromUOM the units for value * @param string $toUOM the units for the result * @@ -2668,93 +1966,6 @@ public static function getConversionMultipliers() */ public static function CONVERTUOM($value, $fromUOM, $toUOM) { - $value = Functions::flattenSingleValue($value); - $fromUOM = Functions::flattenSingleValue($fromUOM); - $toUOM = Functions::flattenSingleValue($toUOM); - - if (!is_numeric($value)) { - return Functions::VALUE(); - } - $fromMultiplier = 1.0; - if (isset(self::$conversionUnits[$fromUOM])) { - $unitGroup1 = self::$conversionUnits[$fromUOM]['Group']; - } else { - $fromMultiplier = substr($fromUOM, 0, 1); - $fromUOM = substr($fromUOM, 1); - if (isset(self::$conversionMultipliers[$fromMultiplier])) { - $fromMultiplier = self::$conversionMultipliers[$fromMultiplier]['multiplier']; - } else { - return Functions::NA(); - } - if ((isset(self::$conversionUnits[$fromUOM])) && (self::$conversionUnits[$fromUOM]['AllowPrefix'])) { - $unitGroup1 = self::$conversionUnits[$fromUOM]['Group']; - } else { - return Functions::NA(); - } - } - $value *= $fromMultiplier; - - $toMultiplier = 1.0; - if (isset(self::$conversionUnits[$toUOM])) { - $unitGroup2 = self::$conversionUnits[$toUOM]['Group']; - } else { - $toMultiplier = substr($toUOM, 0, 1); - $toUOM = substr($toUOM, 1); - if (isset(self::$conversionMultipliers[$toMultiplier])) { - $toMultiplier = self::$conversionMultipliers[$toMultiplier]['multiplier']; - } else { - return Functions::NA(); - } - if ((isset(self::$conversionUnits[$toUOM])) && (self::$conversionUnits[$toUOM]['AllowPrefix'])) { - $unitGroup2 = self::$conversionUnits[$toUOM]['Group']; - } else { - return Functions::NA(); - } - } - if ($unitGroup1 != $unitGroup2) { - return Functions::NA(); - } - - if (($fromUOM == $toUOM) && ($fromMultiplier == $toMultiplier)) { - // We've already factored $fromMultiplier into the value, so we need - // to reverse it again - return $value / $fromMultiplier; - } elseif ($unitGroup1 == 'Temperature') { - if (($fromUOM == 'F') || ($fromUOM == 'fah')) { - if (($toUOM == 'F') || ($toUOM == 'fah')) { - return $value; - } - $value = (($value - 32) / 1.8); - if (($toUOM == 'K') || ($toUOM == 'kel')) { - $value += 273.15; - } - - return $value; - } elseif ( - (($fromUOM == 'K') || ($fromUOM == 'kel')) && - (($toUOM == 'K') || ($toUOM == 'kel')) - ) { - return $value; - } elseif ( - (($fromUOM == 'C') || ($fromUOM == 'cel')) && - (($toUOM == 'C') || ($toUOM == 'cel')) - ) { - return $value; - } - if (($toUOM == 'F') || ($toUOM == 'fah')) { - if (($fromUOM == 'K') || ($fromUOM == 'kel')) { - $value -= 273.15; - } - - return ($value * 1.8) + 32; - } - if (($toUOM == 'C') || ($toUOM == 'cel')) { - return $value - 273.15; - } - - return $value + 273.15; - } - - return ($value * self::$unitConversions[$unitGroup1][$fromUOM][$toUOM]) / $toMultiplier; + return Engineering\ConvertUOM::CONVERT($value, $fromUOM, $toUOM); } } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php new file mode 100644 index 0000000000..0aafe05ec3 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php @@ -0,0 +1,684 @@ + ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Gram', 'AllowPrefix' => true], + 'sg' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Slug', 'AllowPrefix' => false], + 'lbm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Pound mass (avoirdupois)', 'AllowPrefix' => false], + 'u' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U (atomic mass unit)', 'AllowPrefix' => true], + 'ozm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Ounce mass (avoirdupois)', 'AllowPrefix' => false], + 'grain' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Grain', 'AllowPrefix' => false], + 'cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U.S. (short) hundredweight', 'AllowPrefix' => false], + 'shweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U.S. (short) hundredweight', 'AllowPrefix' => false], + 'uk_cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false], + 'lcwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false], + 'hweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false], + 'stone' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Stone', 'AllowPrefix' => false], + 'ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Ton', 'AllowPrefix' => false], + 'uk_ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false], + 'LTON' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false], + 'brton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false], + // Distance + 'm' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Meter', 'AllowPrefix' => true], + 'mi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Statute mile', 'AllowPrefix' => false], + 'Nmi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Nautical mile', 'AllowPrefix' => false], + 'in' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Inch', 'AllowPrefix' => false], + 'ft' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Foot', 'AllowPrefix' => false], + 'yd' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Yard', 'AllowPrefix' => false], + 'ang' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Angstrom', 'AllowPrefix' => true], + 'ell' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Ell', 'AllowPrefix' => false], + 'ly' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Light Year', 'AllowPrefix' => false], + 'parsec' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Parsec', 'AllowPrefix' => false], + 'pc' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Parsec', 'AllowPrefix' => false], + 'Pica' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false], + 'Picapt' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false], + 'pica' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/6 in)', 'AllowPrefix' => false], + 'survey_mi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'U.S survey mile (statute mile)', 'AllowPrefix' => false], + // Time + 'yr' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Year', 'AllowPrefix' => false], + 'day' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Day', 'AllowPrefix' => false], + 'd' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Day', 'AllowPrefix' => false], + 'hr' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Hour', 'AllowPrefix' => false], + 'mn' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Minute', 'AllowPrefix' => false], + 'min' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Minute', 'AllowPrefix' => false], + 'sec' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Second', 'AllowPrefix' => true], + 's' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Second', 'AllowPrefix' => true], + // Pressure + 'Pa' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Pascal', 'AllowPrefix' => true], + 'p' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Pascal', 'AllowPrefix' => true], + 'atm' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true], + 'at' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true], + 'mmHg' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'mm of Mercury', 'AllowPrefix' => true], + 'psi' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'PSI', 'AllowPrefix' => true], + 'Torr' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Torr', 'AllowPrefix' => true], + // Force + 'N' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Newton', 'AllowPrefix' => true], + 'dyn' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Dyne', 'AllowPrefix' => true], + 'dy' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Dyne', 'AllowPrefix' => true], + 'lbf' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Pound force', 'AllowPrefix' => false], + 'pond' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Pond', 'AllowPrefix' => true], + // Energy + 'J' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Joule', 'AllowPrefix' => true], + 'e' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Erg', 'AllowPrefix' => true], + 'c' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Thermodynamic calorie', 'AllowPrefix' => true], + 'cal' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'IT calorie', 'AllowPrefix' => true], + 'eV' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Electron volt', 'AllowPrefix' => true], + 'ev' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Electron volt', 'AllowPrefix' => true], + 'HPh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false], + 'hh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false], + 'Wh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true], + 'wh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true], + 'flb' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Foot-pound', 'AllowPrefix' => false], + 'BTU' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'BTU', 'AllowPrefix' => false], + 'btu' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'BTU', 'AllowPrefix' => false], + // Power + 'HP' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Horsepower', 'AllowPrefix' => false], + 'h' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Horsepower', 'AllowPrefix' => false], + 'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], + 'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], + 'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false], + 'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true], + 'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true], + // Temperature + 'C' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Celsius', 'AllowPrefix' => false], + 'cel' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Celsius', 'AllowPrefix' => false], + 'F' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Fahrenheit', 'AllowPrefix' => false], + 'fah' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Fahrenheit', 'AllowPrefix' => false], + 'K' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Kelvin', 'AllowPrefix' => false], + 'kel' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Kelvin', 'AllowPrefix' => false], + 'Rank' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Rankine', 'AllowPrefix' => false], + 'Reau' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Réaumur', 'AllowPrefix' => false], + // Volume + 'l' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true], + 'L' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true], + 'lt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true], + 'tsp' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Teaspoon', 'AllowPrefix' => false], + 'tspm' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Modern Teaspoon', 'AllowPrefix' => false], + 'tbs' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Tablespoon', 'AllowPrefix' => false], + 'oz' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Fluid Ounce', 'AllowPrefix' => false], + 'cup' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cup', 'AllowPrefix' => false], + 'pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false], + 'us_pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false], + 'uk_pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.K. Pint', 'AllowPrefix' => false], + 'qt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Quart', 'AllowPrefix' => false], + 'uk_qt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Imperial Quart (UK)', 'AllowPrefix' => false], + 'gal' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gallon', 'AllowPrefix' => false], + 'uk_gal' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Imperial Gallon (UK)', 'AllowPrefix' => false], + 'ang3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Angstrom', 'AllowPrefix' => true], + 'ang^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Angstrom', 'AllowPrefix' => true], + 'barrel' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'US Oil Barrel', 'AllowPrefix' => false], + 'bushel' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'US Bushel', 'AllowPrefix' => false], + 'in3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Inch', 'AllowPrefix' => false], + 'in^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Inch', 'AllowPrefix' => false], + 'ft3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Foot', 'AllowPrefix' => false], + 'ft^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Foot', 'AllowPrefix' => false], + 'ly3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Light Year', 'AllowPrefix' => false], + 'ly^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Light Year', 'AllowPrefix' => false], + 'm3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Meter', 'AllowPrefix' => true], + 'm^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Meter', 'AllowPrefix' => true], + 'mi3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Mile', 'AllowPrefix' => false], + 'mi^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Mile', 'AllowPrefix' => false], + 'yd3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Yard', 'AllowPrefix' => false], + 'yd^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Yard', 'AllowPrefix' => false], + 'Nmi3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Nautical Mile', 'AllowPrefix' => false], + 'Nmi^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Nautical Mile', 'AllowPrefix' => false], + 'Pica3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], + 'Pica^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], + 'Picapt3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], + 'Picapt^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], + 'GRT' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gross Registered Ton', 'AllowPrefix' => false], + 'regton' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gross Registered Ton', 'AllowPrefix' => false], + 'MTON' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Measurement Ton (Freight Ton)', 'AllowPrefix' => false], + // Area + 'ha' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Hectare', 'AllowPrefix' => true], + 'uk_acre' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'International Acre', 'AllowPrefix' => false], + 'us_acre' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'US Survey/Statute Acre', 'AllowPrefix' => false], + 'ang2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Angstrom', 'AllowPrefix' => true], + 'ang^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Angstrom', 'AllowPrefix' => true], + 'ar' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Are', 'AllowPrefix' => true], + 'ft2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Feet', 'AllowPrefix' => false], + 'ft^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Feet', 'AllowPrefix' => false], + 'in2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Inches', 'AllowPrefix' => false], + 'in^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Inches', 'AllowPrefix' => false], + 'ly2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Light Years', 'AllowPrefix' => false], + 'ly^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Light Years', 'AllowPrefix' => false], + 'm2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Meters', 'AllowPrefix' => true], + 'm^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Meters', 'AllowPrefix' => true], + 'Morgen' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Morgen', 'AllowPrefix' => false], + 'mi2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Miles', 'AllowPrefix' => false], + 'mi^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Miles', 'AllowPrefix' => false], + 'Nmi2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Nautical Miles', 'AllowPrefix' => false], + 'Nmi^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Nautical Miles', 'AllowPrefix' => false], + 'Pica2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], + 'Pica^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], + 'Picapt2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], + 'Picapt^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], + 'yd2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Yards', 'AllowPrefix' => false], + 'yd^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Yards', 'AllowPrefix' => false], + // Information + 'byte' => ['Group' => self::CATEGORY_INFORMATION, 'Unit Name' => 'Byte', 'AllowPrefix' => true], + 'bit' => ['Group' => self::CATEGORY_INFORMATION, 'Unit Name' => 'Bit', 'AllowPrefix' => true], + // Speed + 'm/s' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per second', 'AllowPrefix' => true], + 'm/sec' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per second', 'AllowPrefix' => true], + 'm/h' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per hour', 'AllowPrefix' => true], + 'm/hr' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per hour', 'AllowPrefix' => true], + 'mph' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Miles per hour', 'AllowPrefix' => false], + 'admkn' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Admiralty Knot', 'AllowPrefix' => false], + 'kn' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Knot', 'AllowPrefix' => false], + ]; + + /** + * Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @var mixed[] + */ + private static $conversionMultipliers = [ + 'Y' => ['multiplier' => 1E24, 'name' => 'yotta'], + 'Z' => ['multiplier' => 1E21, 'name' => 'zetta'], + 'E' => ['multiplier' => 1E18, 'name' => 'exa'], + 'P' => ['multiplier' => 1E15, 'name' => 'peta'], + 'T' => ['multiplier' => 1E12, 'name' => 'tera'], + 'G' => ['multiplier' => 1E9, 'name' => 'giga'], + 'M' => ['multiplier' => 1E6, 'name' => 'mega'], + 'k' => ['multiplier' => 1E3, 'name' => 'kilo'], + 'h' => ['multiplier' => 1E2, 'name' => 'hecto'], + 'e' => ['multiplier' => 1E1, 'name' => 'dekao'], + 'da' => ['multiplier' => 1E1, 'name' => 'dekao'], + 'd' => ['multiplier' => 1E-1, 'name' => 'deci'], + 'c' => ['multiplier' => 1E-2, 'name' => 'centi'], + 'm' => ['multiplier' => 1E-3, 'name' => 'milli'], + 'u' => ['multiplier' => 1E-6, 'name' => 'micro'], + 'n' => ['multiplier' => 1E-9, 'name' => 'nano'], + 'p' => ['multiplier' => 1E-12, 'name' => 'pico'], + 'f' => ['multiplier' => 1E-15, 'name' => 'femto'], + 'a' => ['multiplier' => 1E-18, 'name' => 'atto'], + 'z' => ['multiplier' => 1E-21, 'name' => 'zepto'], + 'y' => ['multiplier' => 1E-24, 'name' => 'yocto'], + ]; + + /** + * Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @var mixed[] + */ + private static $binaryConversionMultipliers = [ + 'Yi' => ['multiplier' => 2 ** 80, 'name' => 'yobi'], + 'Zi' => ['multiplier' => 2 ** 70, 'name' => 'zebi'], + 'Ei' => ['multiplier' => 2 ** 60, 'name' => 'exbi'], + 'Pi' => ['multiplier' => 2 ** 50, 'name' => 'pebi'], + 'Ti' => ['multiplier' => 2 ** 40, 'name' => 'tebi'], + 'Gi' => ['multiplier' => 2 ** 30, 'name' => 'gibi'], + 'Mi' => ['multiplier' => 2 ** 20, 'name' => 'mebi'], + 'ki' => ['multiplier' => 2 ** 10, 'name' => 'kibi'], + ]; + + /** + * Details of the Units of measure conversion factors, organised by group. + * + * @var mixed[] + */ + private static $unitConversions = [ + // Conversion uses gram (g) as an intermediate unit + self::CATEGORY_WEIGHT_AND_MASS => [ + 'g' => 1.0, + 'sg' => 6.85217658567918E-05, + 'lbm' => 2.20462262184878E-03, + 'u' => 6.02214179421676E+23, + 'ozm' => 3.52739619495804E-02, + 'grain' => 1.54323583529414E+01, + 'cwt' => 2.20462262184878E-05, + 'shweight' => 2.20462262184878E-05, + 'uk_cwt' => 1.96841305522212E-05, + 'lcwt' => 1.96841305522212E-05, + 'hweight' => 1.96841305522212E-05, + 'stone' => 1.57473044417770E-04, + 'ton' => 1.10231131092439E-06, + 'uk_ton' => 9.84206527611061E-07, + 'LTON' => 9.84206527611061E-07, + 'brton' => 9.84206527611061E-07, + ], + // Conversion uses meter (m) as an intermediate unit + self::CATEGORY_DISTANCE => [ + 'm' => 1.0, + 'mi' => 6.21371192237334E-04, + 'Nmi' => 5.39956803455724E-04, + 'in' => 3.93700787401575E+01, + 'ft' => 3.28083989501312E+00, + 'yd' => 1.09361329833771E+00, + 'ang' => 1.0E+10, + 'ell' => 8.74890638670166E-01, + 'ly' => 1.05700083402462E-16, + 'parsec' => 3.24077928966473E-17, + 'pc' => 3.24077928966473E-17, + 'Pica' => 2.83464566929134E+03, + 'Picapt' => 2.83464566929134E+03, + 'pica' => 2.36220472440945E+02, + 'survey_mi' => 6.21369949494950E-04, + ], + // Conversion uses second (s) as an intermediate unit + self::CATEGORY_TIME => [ + 'yr' => 3.16880878140289E-08, + 'day' => 1.15740740740741E-05, + 'd' => 1.15740740740741E-05, + 'hr' => 2.77777777777778E-04, + 'mn' => 1.66666666666667E-02, + 'min' => 1.66666666666667E-02, + 'sec' => 1.0, + 's' => 1.0, + ], + // Conversion uses Pascal (Pa) as an intermediate unit + self::CATEGORY_PRESSURE => [ + 'Pa' => 1.0, + 'p' => 1.0, + 'atm' => 9.86923266716013E-06, + 'at' => 9.86923266716013E-06, + 'mmHg' => 7.50063755419211E-03, + 'psi' => 1.45037737730209E-04, + 'Torr' => 7.50061682704170E-03, + ], + // Conversion uses Newton (N) as an intermediate unit + self::CATEGORY_FORCE => [ + 'N' => 1.0, + 'dyn' => 1.0E+5, + 'dy' => 1.0E+5, + 'lbf' => 2.24808923655339E-01, + 'pond' => 1.01971621297793E+02, + ], + // Conversion uses Joule (J) as an intermediate unit + self::CATEGORY_ENERGY => [ + 'J' => 1.0, + 'e' => 9.99999519343231E+06, + 'c' => 2.39006249473467E-01, + 'cal' => 2.38846190642017E-01, + 'eV' => 6.24145700000000E+18, + 'ev' => 6.24145700000000E+18, + 'HPh' => 3.72506430801000E-07, + 'hh' => 3.72506430801000E-07, + 'Wh' => 2.77777916238711E-04, + 'wh' => 2.77777916238711E-04, + 'flb' => 2.37304222192651E+01, + 'BTU' => 9.47815067349015E-04, + 'btu' => 9.47815067349015E-04, + ], + // Conversion uses Horsepower (HP) as an intermediate unit + self::CATEGORY_POWER => [ + 'HP' => 1.0, + 'h' => 1.0, + 'W' => 7.45699871582270E+02, + 'w' => 7.45699871582270E+02, + 'PS' => 1.01386966542400E+00, + ], + // Conversion uses Tesla (T) as an intermediate unit + self::CATEGORY_MAGNETISM => [ + 'T' => 1.0, + 'ga' => 10000.0, + ], + // Conversion uses litre (l) as an intermediate unit + self::CATEGORY_VOLUME => [ + 'l' => 1.0, + 'L' => 1.0, + 'lt' => 1.0, + 'tsp' => 2.02884136211058E+02, + 'tspm' => 2.0E+02, + 'tbs' => 6.76280454036860E+01, + 'oz' => 3.38140227018430E+01, + 'cup' => 4.22675283773038E+00, + 'pt' => 2.11337641886519E+00, + 'us_pt' => 2.11337641886519E+00, + 'uk_pt' => 1.75975398639270E+00, + 'qt' => 1.05668820943259E+00, + 'uk_qt' => 8.79876993196351E-01, + 'gal' => 2.64172052358148E-01, + 'uk_gal' => 2.19969248299088E-01, + 'ang3' => 1.0E+27, + 'ang^3' => 1.0E+27, + 'barrel' => 6.28981077043211E-03, + 'bushel' => 2.83775932584017E-02, + 'in3' => 6.10237440947323E+01, + 'in^3' => 6.10237440947323E+01, + 'ft3' => 3.53146667214886E-02, + 'ft^3' => 3.53146667214886E-02, + 'ly3' => 1.18093498844171E-51, + 'ly^3' => 1.18093498844171E-51, + 'm3' => 1.0E-03, + 'm^3' => 1.0E-03, + 'mi3' => 2.39912758578928E-13, + 'mi^3' => 2.39912758578928E-13, + 'yd3' => 1.30795061931439E-03, + 'yd^3' => 1.30795061931439E-03, + 'Nmi3' => 1.57426214685811E-13, + 'Nmi^3' => 1.57426214685811E-13, + 'Pica3' => 2.27769904358706E+07, + 'Pica^3' => 2.27769904358706E+07, + 'Picapt3' => 2.27769904358706E+07, + 'Picapt^3' => 2.27769904358706E+07, + 'GRT' => 3.53146667214886E-04, + 'regton' => 3.53146667214886E-04, + 'MTON' => 8.82866668037215E-04, + ], + // Conversion uses hectare (ha) as an intermediate unit + self::CATEGORY_AREA => [ + 'ha' => 1.0, + 'uk_acre' => 2.47105381467165E+00, + 'us_acre' => 2.47104393046628E+00, + 'ang2' => 1.0E+24, + 'ang^2' => 1.0E+24, + 'ar' => 1.0E+02, + 'ft2' => 1.07639104167097E+05, + 'ft^2' => 1.07639104167097E+05, + 'in2' => 1.55000310000620E+07, + 'in^2' => 1.55000310000620E+07, + 'ly2' => 1.11725076312873E-28, + 'ly^2' => 1.11725076312873E-28, + 'm2' => 1.0E+04, + 'm^2' => 1.0E+04, + 'Morgen' => 4.0E+00, + 'mi2' => 3.86102158542446E-03, + 'mi^2' => 3.86102158542446E-03, + 'Nmi2' => 2.91553349598123E-03, + 'Nmi^2' => 2.91553349598123E-03, + 'Pica2' => 8.03521607043214E+10, + 'Pica^2' => 8.03521607043214E+10, + 'Picapt2' => 8.03521607043214E+10, + 'Picapt^2' => 8.03521607043214E+10, + 'yd2' => 1.19599004630108E+04, + 'yd^2' => 1.19599004630108E+04, + ], + // Conversion uses bit (bit) as an intermediate unit + self::CATEGORY_INFORMATION => [ + 'bit' => 1.0, + 'byte' => 0.125, + ], + // Conversion uses Meters per Second (m/s) as an intermediate unit + self::CATEGORY_SPEED => [ + 'm/s' => 1.0, + 'm/sec' => 1.0, + 'm/h' => 3.60E+03, + 'm/hr' => 3.60E+03, + 'mph' => 2.23693629205440E+00, + 'admkn' => 1.94260256941567E+00, + 'kn' => 1.94384449244060E+00, + ], + ]; + + /** + * getConversionGroups + * Returns a list of the different conversion groups for UOM conversions. + * + * @return array + */ + public static function getConversionCategories() + { + $conversionGroups = []; + foreach (self::$conversionUnits as $conversionUnit) { + $conversionGroups[] = $conversionUnit['Group']; + } + + return array_merge(array_unique($conversionGroups)); + } + + /** + * getConversionGroupUnits + * Returns an array of units of measure, for a specified conversion group, or for all groups. + * + * @param string $category The group whose units of measure you want to retrieve + * + * @return array + */ + public static function getConversionCategoryUnits($category = null) + { + $conversionGroups = []; + foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { + if (($category === null) || ($conversionGroup['Group'] == $category)) { + $conversionGroups[$conversionGroup['Group']][] = $conversionUnit; + } + } + + return $conversionGroups; + } + + /** + * getConversionGroupUnitDetails. + * + * @param string $category The group whose units of measure you want to retrieve + * + * @return array + */ + public static function getConversionCategoryUnitDetails($category = null) + { + $conversionGroups = []; + foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { + if (($category === null) || ($conversionGroup['Group'] == $category)) { + $conversionGroups[$conversionGroup['Group']][] = [ + 'unit' => $conversionUnit, + 'description' => $conversionGroup['Unit Name'], + ]; + } + } + + return $conversionGroups; + } + + /** + * getConversionMultipliers + * Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @return array of mixed + */ + public static function getConversionMultipliers() + { + return self::$conversionMultipliers; + } + + /** + * getBinaryConversionMultipliers + * Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure in CONVERTUOM(). + * + * @return array of mixed + */ + public static function getBinaryConversionMultipliers() + { + return self::$binaryConversionMultipliers; + } + + /** + * CONVERT. + * + * Converts a number from one measurement system to another. + * For example, CONVERT can translate a table of distances in miles to a table of distances + * in kilometers. + * + * Excel Function: + * CONVERT(value,fromUOM,toUOM) + * + * @param float|int $value the value in fromUOM to convert + * @param string $fromUOM the units for value + * @param string $toUOM the units for the result + * + * @return float|string + */ + public static function CONVERT($value, $fromUOM, $toUOM) + { + $value = Functions::flattenSingleValue($value); + $fromUOM = Functions::flattenSingleValue($fromUOM); + $toUOM = Functions::flattenSingleValue($toUOM); + + if (!is_numeric($value)) { + return Functions::VALUE(); + } + + try { + [$fromUOM, $fromCategory, $fromMultiplier] = self::getUOMDetails($fromUOM); + [$toUOM, $toCategory, $toMultiplier] = self::getUOMDetails($toUOM); + } catch (Exception $e) { + return Functions::NA(); + } + + if ($fromCategory !== $toCategory) { + return Functions::NA(); + } + + $value *= $fromMultiplier; + + if (($fromUOM === $toUOM) && ($fromMultiplier === $toMultiplier)) { + // We've already factored $fromMultiplier into the value, so we need + // to reverse it again + return $value / $fromMultiplier; + } elseif ($fromUOM === $toUOM) { + return $value / $toMultiplier; + } elseif ($fromCategory === self::CATEGORY_TEMPERATURE) { + return self::convertTemperature($fromUOM, $toUOM, $value); + } + + $baseValue = $value * (1.0 / self::$unitConversions[$fromCategory][$fromUOM]); + + return ($baseValue * self::$unitConversions[$fromCategory][$toUOM]) / $toMultiplier; + } + + private static function getUOMDetails(string $uom) + { + if (isset(self::$conversionUnits[$uom])) { + $unitCategory = self::$conversionUnits[$uom]['Group']; + + return [$uom, $unitCategory, 1.0]; + } + + // Check 1-character standard metric multiplier prefixes + $multiplierType = substr($uom, 0, 1); + $uom = substr($uom, 1); + if (isset(self::$conversionUnits[$uom], self::$conversionMultipliers[$multiplierType])) { + if (self::$conversionUnits[$uom]['AllowPrefix'] === false) { + throw new Exception('Prefix not allowed for UoM'); + } + $unitCategory = self::$conversionUnits[$uom]['Group']; + + return [$uom, $unitCategory, self::$conversionMultipliers[$multiplierType]['multiplier']]; + } + + $multiplierType .= substr($uom, 0, 1); + $uom = substr($uom, 1); + + // Check 2-character standard metric multiplier prefixes + if (isset(self::$conversionUnits[$uom], self::$conversionMultipliers[$multiplierType])) { + if (self::$conversionUnits[$uom]['AllowPrefix'] === false) { + throw new Exception('Prefix not allowed for UoM'); + } + $unitCategory = self::$conversionUnits[$uom]['Group']; + + return [$uom, $unitCategory, self::$conversionMultipliers[$multiplierType]['multiplier']]; + } + + // Check 2-character binary multiplier prefixes + if (isset(self::$conversionUnits[$uom], self::$binaryConversionMultipliers[$multiplierType])) { + if (self::$conversionUnits[$uom]['AllowPrefix'] === false) { + throw new Exception('Prefix not allowed for UoM'); + } + $unitCategory = self::$conversionUnits[$uom]['Group']; + if ($unitCategory !== 'Information') { + throw new Exception('Binary Prefix is only allowed for Information UoM'); + } + + return [$uom, $unitCategory, self::$binaryConversionMultipliers[$multiplierType]['multiplier']]; + } + + throw new Exception('UoM Not Found'); + } + + /** + * @param float|int $value + * + * @return float|int + */ + protected static function convertTemperature(string $fromUOM, string $toUOM, $value) + { + $fromUOM = self::resolveTemperatureSynonyms($fromUOM); + $toUOM = self::resolveTemperatureSynonyms($toUOM); + + if ($fromUOM === $toUOM) { + return $value; + } + + // Convert to Kelvin + switch ($fromUOM) { + case 'F': + $value = ($value - 32) / 1.8 + 273.15; + + break; + case 'C': + $value += 273.15; + + break; + case 'Rank': + $value /= 1.8; + + break; + case 'Reau': + $value = $value * 1.25 + 273.15; + + break; + } + + // Convert from Kelvin + switch ($toUOM) { + case 'F': + $value = ($value - 273.15) * 1.8 + 32.00; + + break; + case 'C': + $value -= 273.15; + + break; + case 'Rank': + $value *= 1.8; + + break; + case 'Reau': + $value = ($value - 273.15) * 0.80000; + + break; + } + + return $value; + } + + private static function resolveTemperatureSynonyms(string $uom) + { + switch ($uom) { + case 'fah': + return 'F'; + case 'cel': + return 'C'; + case 'kel': + return 'K'; + } + + return $uom; + } +} diff --git a/tests/data/Calculation/Engineering/CONVERTUOM.php b/tests/data/Calculation/Engineering/CONVERTUOM.php index 248c2cd084..64aaef6244 100644 --- a/tests/data/Calculation/Engineering/CONVERTUOM.php +++ b/tests/data/Calculation/Engineering/CONVERTUOM.php @@ -1,18 +1,72 @@ [ + 1.942559385723E-03, 1.0, - 'lbm', + 'ozm', + 'sg', + ], + 'Same prefixed metric UoM' => [ + 5.0, + 5.0, + 'kg', 'kg', ], - [ - 123.45, - 123.45, + 'Imperial to prefixed metric' => [ + 4.5359237E-01, + 1.0, + 'lbm', 'kg', + ], + 'Prefixed metric to prefixed metric, same unit' => [ + 0.2, + 2.0, + 'hg', 'kg', ], + 'Unprefixed metric to prefixed metric, same unit' => [ + 12.345000000000001, + 12345, + 'm', + 'km', + ], + 'Prefixed metric to unprefixed metric, same unit' => [ + 12345, + 12.345000000000001, + 'km', + 'm', + ], + 'Prefixed metric to imperial' => [ + 0.62137119223732995, + 1, + 'km', + 'mi', + ], + 'Prefixed metric to alternative metric' => [ + 1.23450000000000E+05, + 12.345, + 'um', + 'ang', + ], + 'Prefixed metric to alternative prefixed metric' => [ + 1.23450000000000E+02, + 12.345, + 'um', + 'kang', + ], + 'Prefixed metric to 2-character prefixed metric, same unit' => [ + 1000.0, + 100.0, + 'hl', + 'dal', + ], + 'Imperial to Imperial (distance)' => [ + 1.0, + 3.0, + 'ft', + 'yd', + ], [ 20, 68, @@ -38,111 +92,129 @@ 'F', ], [ - 295.14999999999998, + -273.15, + 0, + 'K', + 'C', + ], + [ + -459.67, + 0, + 'K', + 'F', + ], + [ + 295.15, 22, 'C', 'K', ], [ 22.5, - 295.64999999999998, + 295.65, 'K', 'C', ], - [ - '#N/A', - 2.5, - 'ft', - 'sec', - ], - [ - 12.345000000000001, - 12345, - 'm', - 'km', + 'Melting Point of Titanium (K to C)' => [ + 1667.85, + 1941, + 'K', + 'C', ], - [ - 12345, - 12.345000000000001, - 'km', - 'm', + 'Melting Point of Titanium (K to F)' => [ + 3034.13, + 1941, + 'K', + 'F', ], - [ - 0.62137119223732995, - 1, - 'km', - 'mi', + 'Melting Point of Titanium (K to Rankine)' => [ + 3493.8, + 1941, + 'K', + 'Rank', ], - [ - '#VALUE!', - 'three', - 'ft', - 'yds', + 'Melting Point of Titanium (K to Réaumur)' => [ + 1334.28, + 1941, + 'K', + 'Reau', ], - [ + 'Temperature synonyms (K)' => [ 123.45, 123.45, 'K', 'kel', ], - [ + 'Temperature synonyms (C)' => [ 123.45, 123.45, 'C', 'cel', ], - [ + 'Temperature synonyms (F)' => [ 123.45, 123.45, 'F', 'fah', ], - [ + 'Invalid value to conver' => [ + '#VALUE!', + 'three', + 'ft', + 'yds', + ], + 'Prefixed metric to binary prefixed metric' => [ + '#N/A', + 12.345, + 'um', + 'kiang', + ], + 'Mismatched categories' => [ '#N/A', 1, 'ft', 'day', ], - [ - 123.45, - 123.45, - 'm', - 'm', - ], - [ - 234.56, - 234.56, - 'km', - 'km', - ], - [ + 'From Prefixed imperial (Invalid)' => [ '#N/A', 234.56, 'kpt', 'lt', ], - [ + 'To prefixed imperial (Invalid)' => [ '#N/A', 234.56, - 'sm', - 'm', + 'lt', + 'kpt', ], - [ + 'Invalid from unit' => [ '#N/A', 234.56, - 'lt', - 'kpt', + 'xxxx', + 'm', ], - [ + 'Invalid to unit' => [ '#N/A', 234.56, 'm', - 'sm', + 'xxxx', ], - [ - 12345000, - 12.345000000000001, - 'km', - 'mm', + 'Basic Information conversion' => [ + 2, + 16, + 'bit', + 'byte', + ], + 'Information with standard metric prefix' => [ + 1000, + 1, + 'kbyte', + 'byte', + ], + 'Information with binary prefix' => [ + 1024, + 1, + 'kibyte', + 'byte', ], ]; From 399850967eba23ef6b16ac70f4a93cac411527e9 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 31 Dec 2020 19:03:49 +0100 Subject: [PATCH 34/38] Changelog for 1.16.0 release --- CHANGELOG.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e37e669d..a4741afe0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.16.0 - 2020-12-31 + +### Added + - CSV Reader - Best Guess for Encoding, and Handle Null-string Escape [#1647](https://github.com/PHPOffice/PhpSpreadsheet/issues/1647) ### Changed @@ -26,20 +48,20 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Fix for Xls Reader when SST has a bad length [#1592](https://github.com/PHPOffice/PhpSpreadsheet/issues/1592) -- Resolve Xlsx loader issue whe hyperlinks don't have a destination +- Resolve Xlsx loader issue whe hyperlinks don't have a destination - Resolve issues when printer settings resources IDs clash with drawing IDs - Resolve issue with SLK long filenames [#1612](https://github.com/PHPOffice/PhpSpreadsheet/issues/1612) - ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) - Apply Column and Row Styles to Existing Cells [#1712](https://github.com/PHPOffice/PhpSpreadsheet/issues/1712) [PR #1721](https://github.com/PHPOffice/PhpSpreadsheet/pull/1721) - Resolve issues with defined names where worksheet doesn't exist (#1686)[https://github.com/PHPOffice/PhpSpreadsheet/issues/1686] and [#1723](https://github.com/PHPOffice/PhpSpreadsheet/issues/1723) - [PR #1742](https://github.com/PHPOffice/PhpSpreadsheet/pull/1742) -- Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) +- Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) - Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). - Fix for notice during accessing "cached magnification factor" offset [#1354](https://github.com/PHPOffice/PhpSpreadsheet/pull/1354) - Fix compatibility with ext-gd on php 8 ### Security Fix (CVE-2020-7776) -- Prevent XSS through cell comments in the HTML Writer. +- Prevent XSS through cell comments in the HTML Writer. ## 1.15.0 - 2020-10-11 From c6074abd80ade23d990ba02bdf82adc1e64f74ca Mon Sep 17 00:00:00 2001 From: Martins Sipenko Date: Thu, 7 Jan 2021 12:41:46 +0200 Subject: [PATCH 35/38] Fix date tests withut specified year for current year 2021 (#1774) --- tests/data/Calculation/DateTime/DATEVALUE.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/data/Calculation/DateTime/DATEVALUE.php b/tests/data/Calculation/DateTime/DATEVALUE.php index 0c76eebbc3..17110c749b 100644 --- a/tests/data/Calculation/DateTime/DATEVALUE.php +++ b/tests/data/Calculation/DateTime/DATEVALUE.php @@ -161,12 +161,12 @@ ], // 01/01 of the current year [ - 43831, + 44197, '1 Jan', ], // 31/12 of the current year [ - 44196, + 44561, '31/12', ], // Excel reads as 1st December 1931, not 31st December in current year @@ -176,12 +176,12 @@ ], // 05/07 of the current year [ - 44017, + 44382, '5-JUL', ], // 05/07 of the current year [ - 44017, + 44382, '5 Jul', ], [ From 298dd1e3df63c6ee5fdaf7ee37a6da9412cbbc3f Mon Sep 17 00:00:00 2001 From: Martins Sipenko Date: Thu, 7 Jan 2021 13:06:26 +0200 Subject: [PATCH 36/38] Mrand of zero to any multiple should return 0 (#1773) --- src/PhpSpreadsheet/Calculation/MathTrig.php | 2 +- tests/data/Calculation/MathTrig/MROUND.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index 823f6ef2c8..b8449b55bf 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -851,7 +851,7 @@ public static function MROUND($number, $multiple) $multiple = Functions::flattenSingleValue($multiple); if ((is_numeric($number)) && (is_numeric($multiple))) { - if ($multiple == 0) { + if ($number == 0 || $multiple == 0) { return 0; } if ((self::SIGN($number)) == (self::SIGN($multiple))) { diff --git a/tests/data/Calculation/MathTrig/MROUND.php b/tests/data/Calculation/MathTrig/MROUND.php index 38017b6523..71655485d1 100644 --- a/tests/data/Calculation/MathTrig/MROUND.php +++ b/tests/data/Calculation/MathTrig/MROUND.php @@ -41,6 +41,11 @@ 31415.92654, 1, ], + [ + 0, + 0, + 5, + ], [ '#NUM!', 5, From b00f808fd85e3b93dbb2b8959ff783187a63633f Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Mon, 25 Jan 2021 11:52:04 -0800 Subject: [PATCH 37/38] Inherited Scrutinizer Recommendations - 2 of 3 I tried to sync my fork with the main project, as I have done several times before. However, the GitHub interface to do this had changed, and it appears that I did not make the optimal selection when I had a choice. Consequently, all the merges that happened to base between the last time I synchronized and this time appear to be part of any PR that I push. "Files changed" remains correct for my new PRs, but there appear to be many more commits involved than is actually the case. I will, at some point, delete and re-create my fork, and pay much closer attention in future when I want to sync my fork with the main project. Because of this set-up, Scrutinizer reports flaws in code that I haven't actually changed in my PRs #1799 and #1800. It still passes them, but, as long as I'm aware of the problems, I may as well attempt to correct them. The following are not part of those PRs: - 5 problems spread over 4 different members - 12 problems in Calculation/Engineering - 15 problems in Reader/XML I shall attempt to resolve these via 3 separate PRs, of which this is the second. --- src/PhpSpreadsheet/Calculation/Engineering.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Engineering.php b/src/PhpSpreadsheet/Calculation/Engineering.php index 57116f28f5..00d13c7023 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering.php +++ b/src/PhpSpreadsheet/Calculation/Engineering.php @@ -377,7 +377,7 @@ public static function BINTODEC($x) } } if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $x = floor($x); + $x = floor((float) $x); } $x = (string) $x; if (strlen($x) > preg_match_all('/[01]/', $x, $out)) { @@ -432,7 +432,7 @@ public static function BINTOHEX($x, $places = null) } } if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $x = floor($x); + $x = floor((float) $x); } $x = (string) $x; if (strlen($x) > preg_match_all('/[01]/', $x, $out)) { @@ -442,7 +442,7 @@ public static function BINTOHEX($x, $places = null) return Functions::NAN(); } elseif (strlen($x) == 10) { // Two's Complement - return str_repeat('F', 8) . substr(strtoupper(dechex(bindec(substr($x, -9)))), -2); + return str_repeat('F', 8) . substr(strtoupper(dechex((int) bindec(substr($x, -9)))), -2); } $hexVal = (string) strtoupper(dechex(bindec($x))); @@ -485,7 +485,7 @@ public static function BINTOOCT($x, $places = null) } } if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $x = floor($x); + $x = floor((float) $x); } $x = (string) $x; if (strlen($x) > preg_match_all('/[01]/', $x, $out)) { @@ -495,7 +495,7 @@ public static function BINTOOCT($x, $places = null) return Functions::NAN(); } elseif (strlen($x) == 10) { // Two's Complement - return str_repeat('7', 7) . substr(strtoupper(decoct(bindec(substr($x, -9)))), -3); + return str_repeat('7', 7) . substr(strtoupper(decoct((int) bindec(substr($x, -9)))), -3); } $octVal = (string) decoct(bindec($x)); @@ -546,7 +546,7 @@ public static function DECTOBIN($x, $places = null) return Functions::VALUE(); } - $x = (string) floor($x); + $x = floor((float) $x); if ($x < -512 || $x > 511) { return Functions::NAN(); } @@ -604,7 +604,7 @@ public static function DECTOHEX($x, $places = null) if (strlen($x) > preg_match_all('/[-0123456789.]/', $x, $out)) { return Functions::VALUE(); } - $x = (string) floor($x); + $x = (string) floor((float) $x); $r = strtoupper(dechex($x)); if (strlen($r) == 8) { // Two's Complement @@ -658,7 +658,7 @@ public static function DECTOOCT($x, $places = null) if (strlen($x) > preg_match_all('/[-0123456789.]/', $x, $out)) { return Functions::VALUE(); } - $x = (string) floor($x); + $x = (string) floor((float) $x); $r = decoct($x); if (strlen($r) == 11) { // Two's Complement @@ -945,7 +945,7 @@ public static function OCTTOHEX($x, $places = null) if (preg_match_all('/[01234567]/', $x, $out) != strlen($x)) { return Functions::NAN(); } - $hexVal = strtoupper(dechex(self::OCTTODEC($x))); + $hexVal = strtoupper(dechex(self::OCTTODEC((int) $x))); return self::nbrConversionFormat($hexVal, $places); } From 1d84f4e387affccacf3bd3cf5184c8c7bf27f182 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Mon, 25 Jan 2021 12:25:18 -0800 Subject: [PATCH 38/38] Fixed Most of the Problems, But Some New Ones Cropped Up Trying once more to satisfy Scrutinizer. --- src/PhpSpreadsheet/Calculation/Engineering.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Engineering.php b/src/PhpSpreadsheet/Calculation/Engineering.php index 00d13c7023..319a2ed684 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering.php +++ b/src/PhpSpreadsheet/Calculation/Engineering.php @@ -444,7 +444,7 @@ public static function BINTOHEX($x, $places = null) // Two's Complement return str_repeat('F', 8) . substr(strtoupper(dechex((int) bindec(substr($x, -9)))), -2); } - $hexVal = (string) strtoupper(dechex(bindec($x))); + $hexVal = (string) strtoupper(dechex((int) bindec($x))); return self::nbrConversionFormat($hexVal, $places); } @@ -497,7 +497,7 @@ public static function BINTOOCT($x, $places = null) // Two's Complement return str_repeat('7', 7) . substr(strtoupper(decoct((int) bindec(substr($x, -9)))), -3); } - $octVal = (string) decoct(bindec($x)); + $octVal = (string) decoct((int) bindec($x)); return self::nbrConversionFormat($octVal, $places); } @@ -546,7 +546,7 @@ public static function DECTOBIN($x, $places = null) return Functions::VALUE(); } - $x = floor((float) $x); + $x = (int) floor((float) $x); if ($x < -512 || $x > 511) { return Functions::NAN(); } @@ -604,7 +604,7 @@ public static function DECTOHEX($x, $places = null) if (strlen($x) > preg_match_all('/[-0123456789.]/', $x, $out)) { return Functions::VALUE(); } - $x = (string) floor((float) $x); + $x = (int) floor((float) $x); $r = strtoupper(dechex($x)); if (strlen($r) == 8) { // Two's Complement @@ -658,7 +658,7 @@ public static function DECTOOCT($x, $places = null) if (strlen($x) > preg_match_all('/[-0123456789.]/', $x, $out)) { return Functions::VALUE(); } - $x = (string) floor((float) $x); + $x = (int) floor((float) $x); $r = decoct($x); if (strlen($r) == 11) { // Two's Complement @@ -945,7 +945,7 @@ public static function OCTTOHEX($x, $places = null) if (preg_match_all('/[01234567]/', $x, $out) != strlen($x)) { return Functions::NAN(); } - $hexVal = strtoupper(dechex(self::OCTTODEC((int) $x))); + $hexVal = strtoupper(dechex((int) self::OCTTODEC((int) $x))); return self::nbrConversionFormat($hexVal, $places); }