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