From 968b872c9d09f682113b486830b216f3c96082a1 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 1 Sep 2021 21:52:13 +0100 Subject: [PATCH] MDL-71707 libraries: upgrade to version 3.3.0 of Spout. --- lib/spout/README.md | 12 ++-- lib/spout/readme_moodle.txt | 5 ++ lib/spout/src/Spout/Common/Entity/Cell.php | 2 +- .../src/Spout/Common/Entity/Style/Style.php | 43 ++++++++++++ .../Spout/Common/Helper/FileSystemHelper.php | 10 ++- .../src/Spout/Common/Helper/StringHelper.php | 34 ++++++++++ .../Reader/Common/Manager/RowManager.php | 13 ++++ .../src/Spout/Reader/ODS/SheetIterator.php | 4 +- .../Reader/XLSX/Helper/CellValueFormatter.php | 21 +++--- .../XLSX/Manager/SharedStringsManager.php | 5 +- .../Manager/WorkbookRelationshipsManager.php | 17 +++-- .../src/Spout/Reader/XLSX/RowIterator.php | 2 +- .../Writer/Common/Manager/RegisteredStyle.php | 38 +++++++++++ .../Manager/Style/PossiblyUpdatedStyle.php | 32 +++++++++ .../Common/Manager/Style/StyleManager.php | 22 +++---- .../Manager/Style/StyleManagerInterface.php | 4 +- .../Common/Manager/Style/StyleRegistry.php | 18 +++-- .../Manager/WorkbookManagerAbstract.php | 2 +- .../Writer/ODS/Creator/ManagerFactory.php | 2 +- .../ODS/Manager/Style/StyleRegistry.php | 4 ++ .../Writer/ODS/Manager/WorksheetManager.php | 65 ++++++++++++++----- lib/spout/src/Spout/Writer/WriterAbstract.php | 23 ++++++- .../Writer/XLSX/Creator/ManagerFactory.php | 2 +- .../XLSX/Manager/Style/StyleRegistry.php | 4 ++ .../Writer/XLSX/Manager/WorksheetManager.php | 49 ++++++++++---- lib/thirdpartylibs.xml | 2 +- 26 files changed, 338 insertions(+), 97 deletions(-) create mode 100644 lib/spout/src/Spout/Writer/Common/Manager/RegisteredStyle.php create mode 100644 lib/spout/src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php diff --git a/lib/spout/README.md b/lib/spout/README.md index 3fbb10bdc94..7c772427cdc 100644 --- a/lib/spout/README.md +++ b/lib/spout/README.md @@ -1,26 +1,26 @@ # Spout [![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout) -[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) +[![Project Status](https://opensource.box.com/badges/active.svg)](https://opensource.box.com/badges) [![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) [![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. -Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB). +Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB). -Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) ## Documentation -Full documentation can be found at [http://opensource.box.com/spout/](http://opensource.box.com/spout/). +Full documentation can be found at [https://opensource.box.com/spout/](https://opensource.box.com/spout/). ## Requirements -* PHP version 7.1 or higher +* PHP version 7.2 or higher * PHP extension `php_zip` enabled * PHP extension `php_xmlreader` enabled @@ -43,7 +43,7 @@ For information, the performance tests take about 10 minutes to run (processing ## Support -You can ask questions, submit new features ideas or discuss about Spout in the chat room:
+You can ask questions, submit new features ideas or discuss Spout in the chat room:
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) ## Copyright and License diff --git a/lib/spout/readme_moodle.txt b/lib/spout/readme_moodle.txt index 762598866f4..d1fcc1caaab 100644 --- a/lib/spout/readme_moodle.txt +++ b/lib/spout/readme_moodle.txt @@ -4,6 +4,11 @@ Description of Spout library import * Only include the src/Spout directory. * Update lib/thirdpartylibs.xml with the latest version. +2021/09/01 +---------- +Update to v3.3.0 (MDL-71707) +by Paul Holden + 2020/12/07 ---------- Update to v3.1.0 (MDL-70302) diff --git a/lib/spout/src/Spout/Common/Entity/Cell.php b/lib/spout/src/Spout/Common/Entity/Cell.php index c1de38950b5..174831c29a2 100644 --- a/lib/spout/src/Spout/Common/Entity/Cell.php +++ b/lib/spout/src/Spout/Common/Entity/Cell.php @@ -65,7 +65,7 @@ class Cell protected $style; /** - * @param $value mixed + * @param mixed|null $value * @param Style|null $style */ public function __construct($value, Style $style = null) diff --git a/lib/spout/src/Spout/Common/Entity/Style/Style.php b/lib/spout/src/Spout/Common/Entity/Style/Style.php index 7e989a46d22..2bacc7afa8f 100644 --- a/lib/spout/src/Spout/Common/Entity/Style/Style.php +++ b/lib/spout/src/Spout/Common/Entity/Style/Style.php @@ -84,6 +84,12 @@ class Style /** @var bool */ private $hasSetFormat = false; + /** @var bool */ + private $isRegistered = false; + + /** @var bool */ + private $isEmpty = true; + /** * @return int|null */ @@ -119,6 +125,7 @@ class Style { $this->shouldApplyBorder = true; $this->border = $border; + $this->isEmpty = false; return $this; } @@ -147,6 +154,7 @@ class Style $this->fontBold = true; $this->hasSetFontBold = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -175,6 +183,7 @@ class Style $this->fontItalic = true; $this->hasSetFontItalic = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -203,6 +212,7 @@ class Style $this->fontUnderline = true; $this->hasSetFontUnderline = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -231,6 +241,7 @@ class Style $this->fontStrikethrough = true; $this->hasSetFontStrikethrough = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -260,6 +271,7 @@ class Style $this->fontSize = $fontSize; $this->hasSetFontSize = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -291,6 +303,7 @@ class Style $this->fontColor = $fontColor; $this->hasSetFontColor = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -320,6 +333,7 @@ class Style $this->fontName = $fontName; $this->hasSetFontName = true; $this->shouldApplyFont = true; + $this->isEmpty = false; return $this; } @@ -350,6 +364,7 @@ class Style $this->cellAlignment = $cellAlignment; $this->hasSetCellAlignment = true; $this->shouldApplyCellAlignment = true; + $this->isEmpty = false; return $this; } @@ -386,6 +401,7 @@ class Style { $this->shouldWrapText = $shouldWrap; $this->hasSetWrapText = true; + $this->isEmpty = false; return $this; } @@ -415,6 +431,7 @@ class Style { $this->hasSetBackgroundColor = true; $this->backgroundColor = $color; + $this->isEmpty = false; return $this; } @@ -444,6 +461,7 @@ class Style { $this->hasSetFormat = true; $this->format = $format; + $this->isEmpty = false; return $this; } @@ -463,4 +481,29 @@ class Style { return $this->hasSetFormat; } + + /** + * @return bool + */ + public function isRegistered() : bool + { + return $this->isRegistered; + } + + public function markAsRegistered(?int $id) : void + { + $this->setId($id); + $this->isRegistered = true; + } + + public function unmarkAsRegistered() : void + { + $this->setId(0); + $this->isRegistered = false; + } + + public function isEmpty() : bool + { + return $this->isEmpty; + } } diff --git a/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php b/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php index 4d5d223dc70..4d21fd38c14 100644 --- a/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php +++ b/lib/spout/src/Spout/Common/Helper/FileSystemHelper.php @@ -17,7 +17,7 @@ class FileSystemHelper implements FileSystemHelperInterface /** * @param string $baseFolderPath The path of the base folder where all the I/O can occur */ - public function __construct($baseFolderPath) + public function __construct(string $baseFolderPath) { $this->baseFolderRealPath = \realpath($baseFolderPath); } @@ -117,12 +117,16 @@ class FileSystemHelper implements FileSystemHelperInterface * should occur is not inside the base folder. * * @param string $operationFolderPath The path of the folder where the I/O operation should occur - * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder + * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur + * is not inside the base folder or the base folder does not exist * @return void */ - protected function throwIfOperationNotInBaseFolder($operationFolderPath) + protected function throwIfOperationNotInBaseFolder(string $operationFolderPath) { $operationFolderRealPath = \realpath($operationFolderPath); + if (!$this->baseFolderRealPath) { + throw new IOException("The base folder path is invalid: {$this->baseFolderRealPath}"); + } $isInBaseFolder = (\strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0); if (!$isInBaseFolder) { throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}"); diff --git a/lib/spout/src/Spout/Common/Helper/StringHelper.php b/lib/spout/src/Spout/Common/Helper/StringHelper.php index 09061329d82..6256b1e5f3f 100644 --- a/lib/spout/src/Spout/Common/Helper/StringHelper.php +++ b/lib/spout/src/Spout/Common/Helper/StringHelper.php @@ -13,12 +13,20 @@ class StringHelper /** @var bool Whether the mbstring extension is loaded */ protected $hasMbstringSupport; + /** @var bool Whether the code is running with PHP7 or older versions */ + private $isRunningPhp7OrOlder; + + /** @var array Locale info, used for number formatting */ + private $localeInfo; + /** * */ public function __construct() { $this->hasMbstringSupport = \extension_loaded('mbstring'); + $this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0; + $this->localeInfo = \localeconv(); } /** @@ -68,4 +76,30 @@ class StringHelper return ($position !== false) ? $position : -1; } + + /** + * Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format. + * + * Formatting of float values is locale dependent in PHP < 8. + * Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34). + * However, float values must be formatted with no thousands separator and a "." as decimal point + * to work properly. This method can be used to convert the value to the correct format before storing it. + * + * @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8. + * + * @param int|float $numericValue + * @return string + */ + public function formatNumericValue($numericValue) + { + if ($this->isRunningPhp7OrOlder && is_float($numericValue)) { + return str_replace( + [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']], + ['', '.'], + $numericValue + ); + } + + return $numericValue; + } } diff --git a/lib/spout/src/Spout/Reader/Common/Manager/RowManager.php b/lib/spout/src/Spout/Reader/Common/Manager/RowManager.php index 623c8d8b5c1..67c8fb116e7 100644 --- a/lib/spout/src/Spout/Reader/Common/Manager/RowManager.php +++ b/lib/spout/src/Spout/Reader/Common/Manager/RowManager.php @@ -56,12 +56,25 @@ class RowManager $rowCells = $row->getCells(); $maxCellIndex = $numCells; + // If the row has empty cells, calling "setCellAtIndex" will add the cell + // but in the wrong place (the new cell is added at the end of the array). + // Therefore, we need to sort the array using keys to have proper order. + // @see https://github.com/box/spout/issues/740 + $needsSorting = false; + for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) { if (!isset($rowCells[$cellIndex])) { $row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex); + $needsSorting = true; } } + if ($needsSorting) { + $rowCells = $row->getCells(); + ksort($rowCells); + $row->setCells($rowCells); + } + return $row; } } diff --git a/lib/spout/src/Spout/Reader/ODS/SheetIterator.php b/lib/spout/src/Spout/Reader/ODS/SheetIterator.php index f35b85234d6..c7b8cd9dcf8 100644 --- a/lib/spout/src/Spout/Reader/ODS/SheetIterator.php +++ b/lib/spout/src/Spout/Reader/ODS/SheetIterator.php @@ -28,13 +28,13 @@ class SheetIterator implements IteratorInterface const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name'; const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display'; - /** @var string $filePath Path of the file to be read */ + /** @var string Path of the file to be read */ protected $filePath; /** @var \Box\Spout\Common\Manager\OptionsManagerInterface Reader's options manager */ protected $optionsManager; - /** @var InternalEntityFactory $entityFactory Factory to create entities */ + /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; /** @var XMLReader The XMLReader object that will help read sheet's XML data */ diff --git a/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 169c395be14..ec8368170e7 100644 --- a/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -31,14 +31,6 @@ class CellValueFormatter /** Constants used for date formatting */ const NUM_SECONDS_IN_ONE_DAY = 86400; - const NUM_SECONDS_IN_ONE_HOUR = 3600; - const NUM_SECONDS_IN_ONE_MINUTE = 60; - - /** - * February 29th, 1900 is NOT a leap year but Excel thinks it is... - * @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel - */ - const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60; /** @var SharedStringsManager Manages shared strings */ protected $sharedStringsManager; @@ -130,10 +122,15 @@ class CellValueFormatter */ protected function formatInlineStringCellValue($node) { - // inline strings are formatted this way: - // [INLINE_STRING] - $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0); - $cellValue = $this->escaper->unescape($tNode->nodeValue); + // inline strings are formatted this way (they can contain any number of nodes): + // [INLINE_STRING][INLINE_STRING_2] + $tNodes = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE); + + $cellValue = ''; + for ($i = 0; $i < $tNodes->count(); $i++) { + $tNode = $tNodes->item($i); + $cellValue .= $this->escaper->unescape($tNode->nodeValue); + } return $cellValue; } diff --git a/lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php b/lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php index caaeed767ac..81b4ba29936 100644 --- a/lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php +++ b/lib/spout/src/Spout/Reader/XLSX/Manager/SharedStringsManager.php @@ -16,9 +16,6 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface; */ class SharedStringsManager { - /** Main namespace for the sharedStrings.xml file */ - const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; - /** Definition of XML nodes names used to parse data */ const XML_NODE_SST = 'sst'; const XML_NODE_SI = 'si'; @@ -43,7 +40,7 @@ class SharedStringsManager /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; - /** @var HelperFactory $helperFactory Factory to create helpers */ + /** @var HelperFactory Factory to create helpers */ protected $helperFactory; /** @var CachingStrategyFactory Factory to create shared strings caching strategies */ diff --git a/lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php b/lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php index a153d7830b1..0a9f6f51aa1 100644 --- a/lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php +++ b/lib/spout/src/Spout/Reader/XLSX/Manager/WorkbookRelationshipsManager.php @@ -17,10 +17,11 @@ class WorkbookRelationshipsManager /** Path of workbook relationships XML file inside the XLSX file */ const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels'; - /** Relationships types */ + /** Relationships types - For Transitional and Strict OOXML */ const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'; const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'; - const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; + const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings'; + const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles'; /** Nodes and attributes used to find relevant information in the workbook relationships XML file */ const XML_NODE_RELATIONSHIP = 'Relationship'; @@ -52,7 +53,8 @@ class WorkbookRelationshipsManager public function getSharedStringsXMLFilePath() { $workbookRelationships = $this->getWorkbookRelationships(); - $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]; + $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS] + ?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]; // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml") $doesContainBasePath = (\strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false); @@ -71,7 +73,8 @@ class WorkbookRelationshipsManager { $workbookRelationships = $this->getWorkbookRelationships(); - return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]); + return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]) + || isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]); } /** @@ -81,7 +84,8 @@ class WorkbookRelationshipsManager { $workbookRelationships = $this->getWorkbookRelationships(); - return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]); + return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]) + || isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]); } /** @@ -90,7 +94,8 @@ class WorkbookRelationshipsManager public function getStylesXMLFilePath() { $workbookRelationships = $this->getWorkbookRelationships(); - $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]; + $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES] + ?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]; // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml") $doesContainBasePath = (\strpos($stylesXMLFilePath, self::BASE_PATH) !== false); diff --git a/lib/spout/src/Spout/Reader/XLSX/RowIterator.php b/lib/spout/src/Spout/Reader/XLSX/RowIterator.php index 4af4530d936..a54b8b192b0 100644 --- a/lib/spout/src/Spout/Reader/XLSX/RowIterator.php +++ b/lib/spout/src/Spout/Reader/XLSX/RowIterator.php @@ -35,7 +35,7 @@ class RowIterator implements IteratorInterface /** @var string Path of the XLSX file being read */ protected $filePath; - /** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */ + /** @var string Path of the sheet data XML file as in [Content_Types].xml */ protected $sheetDataXMLFilePath; /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ diff --git a/lib/spout/src/Spout/Writer/Common/Manager/RegisteredStyle.php b/lib/spout/src/Spout/Writer/Common/Manager/RegisteredStyle.php new file mode 100644 index 00000000000..734c2b61da5 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Manager/RegisteredStyle.php @@ -0,0 +1,38 @@ +style = $style; + $this->isMatchingRowStyle = $isMatchingRowStyle; + } + + public function getStyle() : Style + { + return $this->style; + } + + public function isMatchingRowStyle() : bool + { + return $this->isMatchingRowStyle; + } +} diff --git a/lib/spout/src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php b/lib/spout/src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php new file mode 100644 index 00000000000..6ccaa29d953 --- /dev/null +++ b/lib/spout/src/Spout/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php @@ -0,0 +1,32 @@ +style = $style; + $this->isUpdated = $isUpdated; + } + + public function getStyle() : Style + { + return $this->style; + } + + public function isUpdated() : bool + { + return $this->isUpdated; + } +} diff --git a/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManager.php b/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManager.php index 77eec73fb53..e2b5ebdb107 100644 --- a/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManager.php +++ b/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManager.php @@ -50,13 +50,11 @@ class StyleManager implements StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return Style + * @return PossiblyUpdatedStyle The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell) + public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle { - $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($cell); - - return $updatedStyle; + return $this->applyWrapTextIfCellContainsNewLine($cell); } /** @@ -69,21 +67,19 @@ class StyleManager implements StyleManagerInterface * on the Windows version of Excel... * * @param Cell $cell The cell the style should be applied to - * @return \Box\Spout\Common\Entity\Style\Style The eventually updated style + * @return PossiblyUpdatedStyle The eventually updated style */ - protected function applyWrapTextIfCellContainsNewLine(Cell $cell) + protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : PossiblyUpdatedStyle { $cellStyle = $cell->getStyle(); // if the "wrap text" option is already set, no-op - if ($cellStyle->hasSetWrapText()) { - return $cellStyle; - } - - if ($cell->isString() && \strpos($cell->getValue(), "\n") !== false) { + if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) { $cellStyle->setShouldWrapText(); + + return new PossiblyUpdatedStyle($cellStyle, true); } - return $cellStyle; + return new PossiblyUpdatedStyle($cellStyle, false); } } diff --git a/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php b/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php index 312588e0bb3..6b320b1d5b9 100644 --- a/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php +++ b/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleManagerInterface.php @@ -24,7 +24,7 @@ interface StyleManagerInterface * Typically, set "wrap text" if a cell contains a new line. * * @param Cell $cell - * @return Style The updated style + * @return PossiblyUpdatedStyle The eventually updated style */ - public function applyExtraStylesIfNeeded(Cell $cell); + public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle; } diff --git a/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php b/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php index d9e315ff262..6b439a75d64 100644 --- a/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php +++ b/lib/spout/src/Spout/Writer/Common/Manager/Style/StyleRegistry.php @@ -36,9 +36,9 @@ class StyleRegistry { $serializedStyle = $this->serialize($style); - if (!$this->hasStyleAlreadyBeenRegistered($style)) { + if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) { $nextStyleId = \count($this->serializedStyleToStyleIdMappingTable); - $style->setId($nextStyleId); + $style->markAsRegistered($nextStyleId); $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; $this->styleIdToStyleMappingTable[$nextStyleId] = $style; @@ -48,15 +48,13 @@ class StyleRegistry } /** - * Returns whether the given style has already been registered. + * Returns whether the serialized style has already been registered. * - * @param Style $style + * @param string $serializedStyle The serialized style * @return bool */ - protected function hasStyleAlreadyBeenRegistered(Style $style) + protected function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle) { - $serializedStyle = $this->serialize($style); - // Using isset here because it is way faster than array_key_exists... return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); } @@ -101,13 +99,13 @@ class StyleRegistry */ public function serialize(Style $style) { - // In order to be able to properly compare style, set static ID value + // In order to be able to properly compare style, set static ID value and reset registration $currentId = $style->getId(); - $style->setId(0); + $style->unmarkAsRegistered(); $serializedStyle = \serialize($style); - $style->setId($currentId); + $style->markAsRegistered($currentId); return $serializedStyle; } diff --git a/lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php b/lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php index b51355551f9..653778c7047 100644 --- a/lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/lib/spout/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php @@ -44,7 +44,7 @@ abstract class WorkbookManagerAbstract implements WorkbookManagerInterface /** @var InternalEntityFactory Factory to create entities */ protected $entityFactory; - /** @var ManagerFactoryInterface $managerFactory Factory to create managers */ + /** @var ManagerFactoryInterface Factory to create managers */ protected $managerFactory; /** @var Worksheet The worksheet where data will be written to */ diff --git a/lib/spout/src/Spout/Writer/ODS/Creator/ManagerFactory.php b/lib/spout/src/Spout/Writer/ODS/Creator/ManagerFactory.php index f38c5000b0f..a5b77ee42ca 100644 --- a/lib/spout/src/Spout/Writer/ODS/Creator/ManagerFactory.php +++ b/lib/spout/src/Spout/Writer/ODS/Creator/ManagerFactory.php @@ -22,7 +22,7 @@ class ManagerFactory implements ManagerFactoryInterface /** @var InternalEntityFactory */ protected $entityFactory; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** diff --git a/lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php b/lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php index 6c580d49c7a..42484f29294 100644 --- a/lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php +++ b/lib/spout/src/Spout/Writer/ODS/Manager/Style/StyleRegistry.php @@ -22,6 +22,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry */ public function registerStyle(Style $style) { + if ($style->isRegistered()) { + return $style; + } + $registeredStyle = parent::registerStyle($style); $this->usedFontsSet[$style->getFontName()] = true; diff --git a/lib/spout/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/lib/spout/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 4dfe9c8361f..7d7cb0ebb92 100644 --- a/lib/spout/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/lib/spout/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -10,6 +10,7 @@ use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\Escaper\ODS as ODSEscaper; use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Writer\Common\Entity\Worksheet; +use Box\Spout\Writer\Common\Manager\RegisteredStyle; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; use Box\Spout\Writer\ODS\Manager\Style\StyleManager; @@ -93,7 +94,7 @@ class WorksheetManager implements WorksheetManagerInterface $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName()); $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1); - $tableElement = ''; + $tableElement = ''; $tableElement .= ''; return $tableElement; @@ -104,8 +105,8 @@ class WorksheetManager implements WorksheetManagerInterface * * @param Worksheet $worksheet The worksheet to add the row to * @param Row $row The row to be added - * @throws IOException If the data cannot be written * @throws InvalidArgumentException If a cell value's type is not supported + * @throws IOException If the data cannot be written * @return void */ public function addRow(Worksheet $worksheet, Row $row) @@ -125,7 +126,13 @@ class WorksheetManager implements WorksheetManagerInterface $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null; if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) { - $data .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $currentCellIndex, $nextCellIndex); + $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); + $cellStyle = $registeredStyle->getStyle(); + if ($registeredStyle->isMatchingRowStyle()) { + $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id) + } + + $data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex); $currentCellIndex = $nextCellIndex; } @@ -146,24 +153,46 @@ class WorksheetManager implements WorksheetManagerInterface /** * Applies styles to the given style, merging the cell's style with its row's style - * Then builds and returns xml for the cell. * * @param Cell $cell * @param Style $rowStyle - * @param int $currentCellIndex - * @param int $nextCellIndex * @throws InvalidArgumentException If a cell value's type is not supported - * @return string + * @return RegisteredStyle */ - private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex) + private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle { - // Apply row and extra styles - $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); - $cell->setStyle($mergedCellAndRowStyle); - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $isMatchingRowStyle = false; + if ($cell->getStyle()->isEmpty()) { + $cell->setStyle($rowStyle); - $registeredStyle = $this->styleManager->registerStyle($newCellStyle); - $styleIndex = $registeredStyle->getId() + 1; // 1-based + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + + if ($possiblyUpdatedStyle->isUpdated()) { + $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle()); + } else { + $registeredStyle = $this->styleManager->registerStyle($rowStyle); + $isMatchingRowStyle = true; + } + } else { + $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); + $cell->setStyle($mergedCellAndRowStyle); + + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + if ($possiblyUpdatedStyle->isUpdated()) { + $newCellStyle = $possiblyUpdatedStyle->getStyle(); + } else { + $newCellStyle = $mergedCellAndRowStyle; + } + + $registeredStyle = $this->styleManager->registerStyle($newCellStyle); + } + + return new RegisteredStyle($registeredStyle, $isMatchingRowStyle); + } + + private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string + { + $styleIndex = $style->getId() + 1; // 1-based $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex); @@ -197,12 +226,14 @@ class WorksheetManager implements WorksheetManagerInterface $data .= ''; } elseif ($cell->isBoolean()) { - $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cell->getValue() . '">'; + $value = $cell->getValue() ? 'true' : 'false'; // boolean-value spec: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean + $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">'; $data .= '' . $cell->getValue() . ''; $data .= ''; } elseif ($cell->isNumeric()) { - $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">'; - $data .= '' . $cell->getValue() . ''; + $cellValue = $this->stringHelper->formatNumericValue($cell->getValue()); + $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">'; + $data .= '' . $cellValue . ''; $data .= ''; } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) { // only writes the error value if it's a string diff --git a/lib/spout/src/Spout/Writer/WriterAbstract.php b/lib/spout/src/Spout/Writer/WriterAbstract.php index d96a6280fab..36a583fe62f 100644 --- a/lib/spout/src/Spout/Writer/WriterAbstract.php +++ b/lib/spout/src/Spout/Writer/WriterAbstract.php @@ -33,7 +33,7 @@ abstract class WriterAbstract implements WriterInterface /** @var GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** @var OptionsManagerInterface Writer options manager */ @@ -123,9 +123,26 @@ abstract class WriterAbstract implements WriterInterface // @see https://github.com/box/spout/issues/241 $this->globalFunctionsHelper->ob_end_clean(); - // Set headers + /* + * Set headers + * + * For newer browsers such as Firefox, Chrome, Opera, Safari, etc., they all support and use `filename*` + * specified by the new standard, even if they do not automatically decode filename; it does not matter; + * and for older versions of Internet Explorer, they are not recognized `filename*`, will automatically + * ignore it and use the old `filename` (the only minor flaw is that there must be an English suffix name). + * In this way, the multi-browser multi-language compatibility problem is perfectly solved, which does not + * require UA judgment and is more in line with the standard. + * + * @see https://github.com/box/spout/issues/745 + * @see https://tools.ietf.org/html/rfc6266 + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + */ $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType); - $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"'); + $this->globalFunctionsHelper->header( + 'Content-Disposition: attachment; ' . + 'filename="' . rawurldecode($this->outputFilePath) . '"; ' . + 'filename*=UTF-8\'\'' . rawurldecode($this->outputFilePath) + ); /* * When forcing the download of a file over SSL,IE8 and lower browsers fail diff --git a/lib/spout/src/Spout/Writer/XLSX/Creator/ManagerFactory.php b/lib/spout/src/Spout/Writer/XLSX/Creator/ManagerFactory.php index f27a2f2f5e7..aa3bcd5ceac 100644 --- a/lib/spout/src/Spout/Writer/XLSX/Creator/ManagerFactory.php +++ b/lib/spout/src/Spout/Writer/XLSX/Creator/ManagerFactory.php @@ -24,7 +24,7 @@ class ManagerFactory implements ManagerFactoryInterface /** @var InternalEntityFactory */ protected $entityFactory; - /** @var HelperFactory $helperFactory */ + /** @var HelperFactory */ protected $helperFactory; /** diff --git a/lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php b/lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php index ace607ca5d2..14eb9862361 100644 --- a/lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php +++ b/lib/spout/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php @@ -119,6 +119,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry */ public function registerStyle(Style $style) { + if ($style->isRegistered()) { + return $style; + } + $registeredStyle = parent::registerStyle($style); $this->registerFill($registeredStyle); $this->registerFormat($registeredStyle); diff --git a/lib/spout/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/lib/spout/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 741d0aa829f..61b93a17619 100644 --- a/lib/spout/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/lib/spout/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -14,6 +14,7 @@ use Box\Spout\Writer\Common\Creator\InternalEntityFactory; use Box\Spout\Writer\Common\Entity\Options; use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\Common\Helper\CellHelper; +use Box\Spout\Writer\Common\Manager\RegisteredStyle; use Box\Spout\Writer\Common\Manager\RowManager; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface; @@ -160,7 +161,12 @@ EOD; $rowXML = ''; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { - $rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased); + $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); + $cellStyle = $registeredStyle->getStyle(); + if ($registeredStyle->isMatchingRowStyle()) { + $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id) + } + $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId()); } $rowXML .= ''; @@ -173,26 +179,43 @@ EOD; /** * Applies styles to the given style, merging the cell's style with its row's style - * Then builds and returns xml for the cell. * * @param Cell $cell * @param Style $rowStyle - * @param int $rowIndexOneBased - * @param int $columnIndexZeroBased * * @throws InvalidArgumentException If the given value cannot be processed - * @return string + * @return RegisteredStyle */ - private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndexOneBased, $columnIndexZeroBased) + private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle { - // Apply row and extra styles - $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); - $cell->setStyle($mergedCellAndRowStyle); - $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + $isMatchingRowStyle = false; + if ($cell->getStyle()->isEmpty()) { + $cell->setStyle($rowStyle); - $registeredStyle = $this->styleManager->registerStyle($newCellStyle); + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); - return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId()); + if ($possiblyUpdatedStyle->isUpdated()) { + $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle()); + } else { + $registeredStyle = $this->styleManager->registerStyle($rowStyle); + $isMatchingRowStyle = true; + } + } else { + $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); + $cell->setStyle($mergedCellAndRowStyle); + + $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); + + if ($possiblyUpdatedStyle->isUpdated()) { + $newCellStyle = $possiblyUpdatedStyle->getStyle(); + } else { + $newCellStyle = $mergedCellAndRowStyle; + } + + $registeredStyle = $this->styleManager->registerStyle($newCellStyle); + } + + return new RegisteredStyle($registeredStyle, $isMatchingRowStyle); } /** @@ -217,7 +240,7 @@ EOD; } elseif ($cell->isBoolean()) { $cellXML .= ' t="b">' . (int) ($cell->getValue()) . ''; } elseif ($cell->isNumeric()) { - $cellXML .= '>' . $cell->getValue() . ''; + $cellXML .= '>' . $this->stringHelper->formatNumericValue($cell->getValue()) . ''; } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) { // only writes the error value if it's a string $cellXML .= ' t="e">' . $cell->getValueEvenIfError() . ''; diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index df6c731d90d..f325724bcc1 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -221,7 +221,7 @@ spout Spout Apache - 3.1.0 + 3.3.0 2.0