diff --git a/lib/openspout/composer.json b/lib/openspout/composer.json index ffc36db5bd1..b389654260f 100644 --- a/lib/openspout/composer.json +++ b/lib/openspout/composer.json @@ -28,7 +28,7 @@ ], "homepage": "https://github.com/openspout/openspout", "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "ext-dom": "*", "ext-fileinfo": "*", "ext-filter": "*", @@ -38,13 +38,13 @@ }, "require-dev": { "ext-zlib": "*", - "friendsofphp/php-cs-fixer": "^3.46.0", - "infection/infection": "^0.27.9", - "phpbench/phpbench": "^1.2.15", - "phpstan/phpstan": "^1.10.55", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^10.5.5" + "friendsofphp/php-cs-fixer": "^3.68.3", + "infection/infection": "^0.29.10", + "phpbench/phpbench": "^1.4.0", + "phpstan/phpstan": "^2.1.2", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^11.5.4" }, "suggest": { "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)", diff --git a/lib/openspout/readme_moodle.txt b/lib/openspout/readme_moodle.txt index eade7a06406..e9f5e545e41 100644 --- a/lib/openspout/readme_moodle.txt +++ b/lib/openspout/readme_moodle.txt @@ -6,5 +6,5 @@ Downloaded from: https://github.com/openspout/openspout Import procedure: * Copy all the files from the folder 'src' directory. -* Copy the LICENSE & README.md files from the project root. +* Copy the LICENSE, README.md and composer.json files from the root of the project. * Update lib/thirdpartylibs.xml with the latest version. diff --git a/lib/openspout/src/Common/Entity/Cell/FormulaCell.php b/lib/openspout/src/Common/Entity/Cell/FormulaCell.php index 2e07a96c647..e5f098b532c 100644 --- a/lib/openspout/src/Common/Entity/Cell/FormulaCell.php +++ b/lib/openspout/src/Common/Entity/Cell/FormulaCell.php @@ -14,7 +14,7 @@ final class FormulaCell extends Cell public function __construct( private readonly string $value, ?Style $style, - private readonly null|DateInterval|DateTimeImmutable|float|int|string $computedValue = null, + private readonly null|bool|DateInterval|DateTimeImmutable|float|int|string $computedValue = null, ) { parent::__construct($style); } @@ -24,7 +24,7 @@ final class FormulaCell extends Cell return $this->value; } - public function getComputedValue(): null|DateInterval|DateTimeImmutable|float|int|string + public function getComputedValue(): null|bool|DateInterval|DateTimeImmutable|float|int|string { return $this->computedValue; } diff --git a/lib/openspout/src/Common/Entity/Row.php b/lib/openspout/src/Common/Entity/Row.php index 5715b9f959d..60405031fd4 100644 --- a/lib/openspout/src/Common/Entity/Row.php +++ b/lib/openspout/src/Common/Entity/Row.php @@ -48,6 +48,19 @@ final class Row return new self($cells, $rowStyle); } + /** + * @param array $cellValues + * @param array $columnStyles + */ + public static function fromValuesWithStyles(array $cellValues = [], ?Style $rowStyle = null, array $columnStyles = []): self + { + $cells = array_map(static function (null|bool|DateInterval|DateTimeInterface|float|int|string $cellValue, int|string $key) use ($columnStyles): Cell { + return Cell::fromValue($cellValue, $columnStyles[$key] ?? null); + }, $cellValues, array_keys($cellValues)); + + return new self($cells, $rowStyle); + } + /** * @return Cell[] $cells */ diff --git a/lib/openspout/src/Common/Entity/Style/Border.php b/lib/openspout/src/Common/Entity/Style/Border.php index 487edaa3234..695dfe978ea 100644 --- a/lib/openspout/src/Common/Entity/Style/Border.php +++ b/lib/openspout/src/Common/Entity/Style/Border.php @@ -22,7 +22,7 @@ final class Border public const WIDTH_THICK = 'thick'; /** @var array */ - private array $parts; + private array $parts = []; public function __construct(BorderPart ...$borderParts) { diff --git a/lib/openspout/src/Common/Entity/Style/BorderPart.php b/lib/openspout/src/Common/Entity/Style/BorderPart.php index 8bd9a1abbdd..712e82fd8d2 100644 --- a/lib/openspout/src/Common/Entity/Style/BorderPart.php +++ b/lib/openspout/src/Common/Entity/Style/BorderPart.php @@ -8,7 +8,7 @@ use OpenSpout\Writer\Exception\Border\InvalidNameException; use OpenSpout\Writer\Exception\Border\InvalidStyleException; use OpenSpout\Writer\Exception\Border\InvalidWidthException; -final class BorderPart +final readonly class BorderPart { public const allowedStyles = [ Border::STYLE_NONE, @@ -31,10 +31,10 @@ final class BorderPart Border::WIDTH_THICK, ]; - private readonly string $style; - private readonly string $name; - private readonly string $color; - private readonly string $width; + private string $style; + private string $name; + private string $color; + private string $width; /** * @param string $name @see BorderPart::allowedNames diff --git a/lib/openspout/src/Common/Entity/Style/Style.php b/lib/openspout/src/Common/Entity/Style/Style.php index 22e4c7ec56b..98d35ed680c 100644 --- a/lib/openspout/src/Common/Entity/Style/Style.php +++ b/lib/openspout/src/Common/Entity/Style/Style.php @@ -90,6 +90,12 @@ final class Style /** @var bool Whether the wrap text property was set */ private bool $hasSetWrapText = false; + /** @var int Text rotation */ + private int $textRotation = 0; + + /** @var bool Whether the text rotation property was set */ + private bool $hasSetTextRotation = false; + /** @var bool Whether the cell should shrink to fit to content */ private bool $shouldShrinkToFit = false; @@ -383,6 +389,28 @@ final class Style return $this->hasSetWrapText; } + public function textRotation(): int + { + return $this->textRotation; + } + + /** + * @param int $rotation Rotate text + */ + public function setTextRotation(int $rotation): self + { + $this->textRotation = $rotation; + $this->hasSetTextRotation = true; + $this->isEmpty = false; + + return $this; + } + + public function hasSetTextRotation(): bool + { + return $this->hasSetTextRotation; + } + /** * @return bool Whether specific font properties should be applied */ diff --git a/lib/openspout/src/Common/Helper/EncodingHelper.php b/lib/openspout/src/Common/Helper/EncodingHelper.php index 82de2449226..269ded5813e 100644 --- a/lib/openspout/src/Common/Helper/EncodingHelper.php +++ b/lib/openspout/src/Common/Helper/EncodingHelper.php @@ -10,7 +10,7 @@ use OpenSpout\Common\Exception\EncodingConversionException; /** * @internal */ -final class EncodingHelper +final readonly class EncodingHelper { /** * Definition of the encodings that can have a BOM. @@ -33,9 +33,9 @@ final class EncodingHelper /** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */ private array $supportedEncodingsWithBom; - private readonly bool $canUseIconv; + private bool $canUseIconv; - private readonly bool $canUseMbString; + private bool $canUseMbString; public function __construct(bool $canUseIconv, bool $canUseMbString) { diff --git a/lib/openspout/src/Common/Helper/Escaper/XLSX.php b/lib/openspout/src/Common/Helper/Escaper/XLSX.php index 059b7d9bdc7..c1a519a65cb 100644 --- a/lib/openspout/src/Common/Helper/Escaper/XLSX.php +++ b/lib/openspout/src/Common/Helper/Escaper/XLSX.php @@ -107,7 +107,7 @@ final class XLSX implements EscaperInterface $character = \chr($charValue); if (1 === preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { $charHexValue = dechex($charValue); - $escapedChar = '_x'.sprintf('%04s', strtoupper($charHexValue)).'_'; + $escapedChar = '_x'.\sprintf('%04s', strtoupper($charHexValue)).'_'; $controlCharactersEscapingMap[$escapedChar] = $character; } } diff --git a/lib/openspout/src/Common/Helper/FileSystemHelper.php b/lib/openspout/src/Common/Helper/FileSystemHelper.php index 2309bfe68e2..ae8b2edbe3a 100644 --- a/lib/openspout/src/Common/Helper/FileSystemHelper.php +++ b/lib/openspout/src/Common/Helper/FileSystemHelper.php @@ -11,10 +11,10 @@ use RecursiveIteratorIterator; /** * @internal */ -final class FileSystemHelper implements FileSystemHelperInterface +final readonly class FileSystemHelper implements FileSystemHelperInterface { /** @var string Real path of the base folder where all the I/O can occur */ - private readonly string $baseFolderRealPath; + private string $baseFolderRealPath; /** * @param string $baseFolderPath The path of the base folder where all the I/O can occur diff --git a/lib/openspout/src/Common/Helper/StringHelper.php b/lib/openspout/src/Common/Helper/StringHelper.php index fb5bada0a5e..78539b58c0c 100644 --- a/lib/openspout/src/Common/Helper/StringHelper.php +++ b/lib/openspout/src/Common/Helper/StringHelper.php @@ -7,10 +7,10 @@ namespace OpenSpout\Common\Helper; /** * @internal */ -final class StringHelper +final readonly class StringHelper { /** @var bool Whether the mbstring extension is loaded */ - private readonly bool $hasMbstringSupport; + private bool $hasMbstringSupport; public function __construct(bool $hasMbstringSupport) { diff --git a/lib/openspout/src/Reader/CSV/RowIterator.php b/lib/openspout/src/Reader/CSV/RowIterator.php index 81f50bd7a3d..938672d7473 100644 --- a/lib/openspout/src/Reader/CSV/RowIterator.php +++ b/lib/openspout/src/Reader/CSV/RowIterator.php @@ -134,7 +134,7 @@ final class RowIterator implements RowIteratorInterface if (false !== $rowData) { // array_map will replace NULL values by empty strings - $rowDataBufferAsArray = array_map('\\strval', $rowData); + $rowDataBufferAsArray = array_map('\strval', $rowData); $this->rowBuffer = new Row(array_map(static function ($cellValue) { return Cell::fromValue($cellValue); }, $rowDataBufferAsArray), null); diff --git a/lib/openspout/src/Reader/CSV/Sheet.php b/lib/openspout/src/Reader/CSV/Sheet.php index 142e6255e20..ea37e24b4a1 100644 --- a/lib/openspout/src/Reader/CSV/Sheet.php +++ b/lib/openspout/src/Reader/CSV/Sheet.php @@ -9,10 +9,10 @@ use OpenSpout\Reader\SheetInterface; /** * @implements SheetInterface */ -final class Sheet implements SheetInterface +final readonly class Sheet implements SheetInterface { /** @var RowIterator To iterate over the CSV's rows */ - private readonly RowIterator $rowIterator; + private RowIterator $rowIterator; /** * @param RowIterator $rowIterator Corresponding row iterator diff --git a/lib/openspout/src/Reader/Common/ColumnWidth.php b/lib/openspout/src/Reader/Common/ColumnWidth.php index 73ecb627a5a..2e8aac31f22 100644 --- a/lib/openspout/src/Reader/Common/ColumnWidth.php +++ b/lib/openspout/src/Reader/Common/ColumnWidth.php @@ -7,15 +7,15 @@ namespace OpenSpout\Reader\Common; /** * @internal */ -final class ColumnWidth +final readonly class ColumnWidth { /** * @param positive-int $start * @param positive-int $end */ public function __construct( - public readonly int $start, - public readonly int $end, - public readonly float $width, + public int $start, + public int $end, + public float $width, ) {} } diff --git a/lib/openspout/src/Reader/ODS/Helper/CellValueFormatter.php b/lib/openspout/src/Reader/ODS/Helper/CellValueFormatter.php index f525a471bcc..cd3efcc758c 100644 --- a/lib/openspout/src/Reader/ODS/Helper/CellValueFormatter.php +++ b/lib/openspout/src/Reader/ODS/Helper/CellValueFormatter.php @@ -16,7 +16,7 @@ use OpenSpout\Reader\Exception\InvalidValueException; /** * @internal */ -final class CellValueFormatter +final readonly class CellValueFormatter { /** * Definition of all possible cell types. @@ -61,10 +61,10 @@ final class CellValueFormatter ]; /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ - private readonly bool $shouldFormatDates; + private bool $shouldFormatDates; /** @var ODS Used to unescape XML data */ - private readonly ODS $escaper; + private ODS $escaper; /** * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings diff --git a/lib/openspout/src/Reader/ODS/RowIterator.php b/lib/openspout/src/Reader/ODS/RowIterator.php index cfb6eeafc9b..ca6c2bdab3b 100644 --- a/lib/openspout/src/Reader/ODS/RowIterator.php +++ b/lib/openspout/src/Reader/ODS/RowIterator.php @@ -37,8 +37,8 @@ final class RowIterator implements RowIteratorInterface /** @var XMLProcessor Helper Object to process XML nodes */ private readonly XMLProcessor $xmlProcessor; - /** @var Helper\CellValueFormatter Helper to format cell values */ - private readonly Helper\CellValueFormatter $cellValueFormatter; + /** @var CellValueFormatter Helper to format cell values */ + private readonly CellValueFormatter $cellValueFormatter; /** @var bool Whether the iterator has already been rewound once */ private bool $hasAlreadyBeenRewound = false; diff --git a/lib/openspout/src/Reader/ODS/Sheet.php b/lib/openspout/src/Reader/ODS/Sheet.php index 52917b58639..718b3599485 100644 --- a/lib/openspout/src/Reader/ODS/Sheet.php +++ b/lib/openspout/src/Reader/ODS/Sheet.php @@ -9,22 +9,22 @@ use OpenSpout\Reader\SheetWithVisibilityInterface; /** * @implements SheetWithVisibilityInterface */ -final class Sheet implements SheetWithVisibilityInterface +final readonly class Sheet implements SheetWithVisibilityInterface { /** @var RowIterator To iterate over sheet's rows */ - private readonly RowIterator $rowIterator; + private RowIterator $rowIterator; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ - private readonly int $index; + private int $index; /** @var string Name of the sheet */ - private readonly string $name; + private string $name; /** @var bool Whether the sheet was the active one */ - private readonly bool $isActive; + private bool $isActive; /** @var bool Whether the sheet is visible */ - private readonly bool $isVisible; + private bool $isVisible; /** * @param RowIterator $rowIterator The corresponding row iterator diff --git a/lib/openspout/src/Reader/SheetWithMergeCellsInterface.php b/lib/openspout/src/Reader/SheetWithMergeCellsInterface.php new file mode 100644 index 00000000000..2367e2c68de --- /dev/null +++ b/lib/openspout/src/Reader/SheetWithMergeCellsInterface.php @@ -0,0 +1,18 @@ + + */ +interface SheetWithMergeCellsInterface extends SheetInterface +{ + /** + * @return list Merge cells list ["C7:E7", "A9:D10"] + */ + public function getMergeCells(): array; +} diff --git a/lib/openspout/src/Reader/Wrapper/XMLReader.php b/lib/openspout/src/Reader/Wrapper/XMLReader.php index c2ebc16bd2d..ca375af4518 100644 --- a/lib/openspout/src/Reader/Wrapper/XMLReader.php +++ b/lib/openspout/src/Reader/Wrapper/XMLReader.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace OpenSpout\Reader\Wrapper; +use OpenSpout\Common\Exception\IOException; use OpenSpout\Reader\Exception\XMLProcessingException; use ZipArchive; @@ -52,7 +53,12 @@ final class XMLReader extends \XMLReader // The file path should not start with a '/', otherwise it won't be found $fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/'); - return self::ZIP_WRAPPER.realpath($zipFilePath).'#'.$fileInsideZipPathWithoutLeadingSlash; + $realpath = realpath($zipFilePath); + if (false === $realpath) { + throw new IOException("Could not open {$zipFilePath} for reading! File does not exist."); + } + + return self::ZIP_WRAPPER.$realpath.'#'.$fileInsideZipPathWithoutLeadingSlash; } /** diff --git a/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php b/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php index 0157f5c2b9c..596e4f6b92d 100644 --- a/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php +++ b/lib/openspout/src/Reader/XLSX/Helper/CellValueFormatter.php @@ -17,7 +17,7 @@ use OpenSpout\Reader\XLSX\Manager\StyleManagerInterface; /** * This class provides helper functions to format cell values. */ -final class CellValueFormatter +final readonly class CellValueFormatter { /** * Definition of all possible cell types. @@ -49,19 +49,19 @@ final class CellValueFormatter public const NUM_SECONDS_IN_ONE_DAY = 86400; /** @var SharedStringsManager Manages shared strings */ - private readonly SharedStringsManager $sharedStringsManager; + private SharedStringsManager $sharedStringsManager; /** @var StyleManagerInterface Manages styles */ - private readonly StyleManagerInterface $styleManager; + private StyleManagerInterface $styleManager; /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ - private readonly bool $shouldFormatDates; + private bool $shouldFormatDates; /** @var bool Whether date/time values should use a calendar starting in 1904 instead of 1900 */ - private readonly bool $shouldUse1904Dates; + private bool $shouldUse1904Dates; /** @var XLSX Used to unescape XML data */ - private readonly XLSX $escaper; + private XLSX $escaper; /** * @param SharedStringsManager $sharedStringsManager Manages shared strings @@ -96,28 +96,22 @@ final class CellValueFormatter } $vNodeValue = $this->getVNodeValue($node); - if (self::CELL_TYPE_NUMERIC === $cellType) { - $fNodeValue = $node->getElementsByTagName(self::XML_NODE_FORMULA)->item(0)?->nodeValue; - if (null !== $fNodeValue) { - $computedValue = $this->formatNumericCellValue($vNodeValue, (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)); + $fNodeValue = $node->getElementsByTagName(self::XML_NODE_FORMULA)->item(0)?->nodeValue; + if (null !== $fNodeValue) { + $computedValue = $this->formatRawValueForCellType($cellType, $node, $vNodeValue); - return new Cell\FormulaCell('='.$fNodeValue, null, $computedValue); - } + return new Cell\FormulaCell( + '='.$fNodeValue, + null, + $computedValue instanceof Cell\ErrorCell ? null : $computedValue + ); } if ('' === $vNodeValue && self::CELL_TYPE_INLINE_STRING !== $cellType) { return Cell::fromValue($vNodeValue); } - $rawValue = match ($cellType) { - self::CELL_TYPE_INLINE_STRING => $this->formatInlineStringCellValue($node), - self::CELL_TYPE_SHARED_STRING => $this->formatSharedStringCellValue($vNodeValue), - self::CELL_TYPE_STR => $this->formatStrCellValue($vNodeValue), - self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($vNodeValue), - self::CELL_TYPE_NUMERIC => $this->formatNumericCellValue($vNodeValue, (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)), - self::CELL_TYPE_DATE => $this->formatDateCellValue($vNodeValue), - default => new Cell\ErrorCell($vNodeValue, null), - }; + $rawValue = $this->formatRawValueForCellType($cellType, $node, $vNodeValue); if ($rawValue instanceof Cell) { return $rawValue; @@ -328,4 +322,23 @@ final class CellValueFormatter return $cellValue; } + + private function formatRawValueForCellType( + string $cellType, + DOMElement $node, + string $vNodeValue + ): bool|Cell\ErrorCell|DateInterval|DateTimeImmutable|float|int|string { + return match ($cellType) { + self::CELL_TYPE_INLINE_STRING => $this->formatInlineStringCellValue($node), + self::CELL_TYPE_SHARED_STRING => $this->formatSharedStringCellValue($vNodeValue), + self::CELL_TYPE_STR => $this->formatStrCellValue($vNodeValue), + self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($vNodeValue), + self::CELL_TYPE_NUMERIC => $this->formatNumericCellValue( + $vNodeValue, + (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID) + ), + self::CELL_TYPE_DATE => $this->formatDateCellValue($vNodeValue), + default => new Cell\ErrorCell($vNodeValue, null), + }; + } } diff --git a/lib/openspout/src/Reader/XLSX/Helper/DateIntervalFormatHelper.php b/lib/openspout/src/Reader/XLSX/Helper/DateIntervalFormatHelper.php index 7ae1a6e5be1..61d5d3460b4 100644 --- a/lib/openspout/src/Reader/XLSX/Helper/DateIntervalFormatHelper.php +++ b/lib/openspout/src/Reader/XLSX/Helper/DateIntervalFormatHelper.php @@ -61,13 +61,16 @@ final class DateIntervalFormatHelper return 1 === preg_match('/^(\[hh?](:mm(:ss)?)?|\[mm?](:ss)?|\[ss?])$/', $excelFormat); } - public static function toPHPDateIntervalFormat(string $excelDateFormat, ?string &$startUnit = null): string + public static function toPHPDateIntervalFormat(string $excelDateFormat, string &$startUnit): string { - $startUnit = null; + $startUnitStarted = false; $phpFormatParts = []; $formatParts = explode(':', str_replace(['[', ']'], '', $excelDateFormat)); foreach ($formatParts as $formatPart) { - $startUnit ??= $formatPart; + if (false === $startUnitStarted) { + $startUnit = $formatPart; + $startUnitStarted = true; + } $phpFormatParts[] = self::dateIntervalFormats[$formatPart]; } @@ -77,6 +80,7 @@ final class DateIntervalFormatHelper public static function formatDateInterval(DateInterval $dateInterval, string $excelDateFormat): string { + $startUnit = ''; $phpFormat = self::toPHPDateIntervalFormat($excelDateFormat, $startUnit); // We have to move the hours to minutes or hours+minutes to seconds if the format in Excel did the same: diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php index 749e628537c..65ca6bc7829 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/CachingStrategyFactory.php @@ -7,7 +7,7 @@ namespace OpenSpout\Reader\XLSX\Manager\SharedStringsCaching; /** * @internal */ -final class CachingStrategyFactory implements CachingStrategyFactoryInterface +final readonly class CachingStrategyFactory implements CachingStrategyFactoryInterface { /** * The memory amount needed to store a string was obtained empirically from this data:. @@ -50,7 +50,7 @@ final class CachingStrategyFactory implements CachingStrategyFactoryInterface */ public const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000; - private readonly MemoryLimit $memoryLimit; + private MemoryLimit $memoryLimit; public function __construct(MemoryLimit $memoryLimit) { diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/FileBasedStrategy.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/FileBasedStrategy.php index 2e9dc35e9c1..46cac3cc932 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/FileBasedStrategy.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/FileBasedStrategy.php @@ -42,7 +42,10 @@ final class FileBasedStrategy implements CachingStrategyInterface * * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE */ - private string $inMemoryTempFilePath = ''; + private string $readMemoryTempFilePath = ''; + + /** @var string Path of the temporary file whose contents is currently being written to */ + private string $writeMemoryTempFilePath = ''; /** * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE @@ -73,13 +76,14 @@ final class FileBasedStrategy implements CachingStrategyInterface { $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); - if (!file_exists($tempFilePath)) { + if ($this->writeMemoryTempFilePath !== $tempFilePath) { if (null !== $this->tempFilePointer) { fclose($this->tempFilePointer); } $resource = fopen($tempFilePath, 'w'); \assert(false !== $resource); $this->tempFilePointer = $resource; + $this->writeMemoryTempFilePath = $tempFilePath; } // The shared string retrieval logic expects each cell data to be on one line only @@ -97,6 +101,7 @@ final class FileBasedStrategy implements CachingStrategyInterface { // close pointer to the last temp file that was written if (null !== $this->tempFilePointer) { + $this->writeMemoryTempFilePath = ''; fclose($this->tempFilePointer); } } @@ -115,17 +120,13 @@ final class FileBasedStrategy implements CachingStrategyInterface $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile; - if (!file_exists($tempFilePath)) { - throw new SharedStringNotFoundException("Shared string temp file not found: {$tempFilePath} ; for index: {$sharedStringIndex}"); - } - - if ($this->inMemoryTempFilePath !== $tempFilePath) { - $tempFilePath = realpath($tempFilePath); - \assert(false !== $tempFilePath); - $contents = file_get_contents($tempFilePath); - \assert(false !== $contents); + if ($this->readMemoryTempFilePath !== $tempFilePath) { + $contents = @file_get_contents($tempFilePath); + if (false === $contents) { + throw new SharedStringNotFoundException("Shared string temp file could not be read: {$tempFilePath} ; for index: {$sharedStringIndex}"); + } $this->inMemoryTempFileContents = explode(PHP_EOL, $contents); - $this->inMemoryTempFilePath = $tempFilePath; + $this->readMemoryTempFilePath = $tempFilePath; } $sharedString = null; diff --git a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php index 5645908f1bf..a77025088f6 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SharedStringsCaching/MemoryLimit.php @@ -7,9 +7,9 @@ namespace OpenSpout\Reader\XLSX\Manager\SharedStringsCaching; /** * @internal */ -final class MemoryLimit +final readonly class MemoryLimit { - private readonly string $memoryLimit; + private string $memoryLimit; public function __construct(string $memoryLimit) { diff --git a/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php b/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php index 69aa11d3611..a015b1bf887 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php +++ b/lib/openspout/src/Reader/XLSX/Manager/SheetManager.php @@ -13,6 +13,7 @@ use OpenSpout\Reader\XLSX\Options; use OpenSpout\Reader\XLSX\RowIterator; use OpenSpout\Reader\XLSX\Sheet; use OpenSpout\Reader\XLSX\SheetHeaderReader; +use OpenSpout\Reader\XLSX\SheetMergeCellsReader; /** * @internal @@ -49,6 +50,7 @@ final class SheetManager * State value to represent a hidden sheet. */ public const SHEET_STATE_HIDDEN = 'hidden'; + public const SHEET_STATE_VERY_HIDDEN = 'veryHidden'; /** @var string Path of the XLSX file being read */ private readonly string $filePath; @@ -178,7 +180,7 @@ final class SheetManager \assert(null !== $sheetId); $sheetState = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_STATE); - $isSheetVisible = (self::SHEET_STATE_HIDDEN !== $sheetState); + $isSheetVisible = (self::SHEET_STATE_HIDDEN !== $sheetState && self::SHEET_STATE_VERY_HIDDEN !== $sheetState); $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME); \assert(null !== $escapedSheetName); @@ -186,13 +188,24 @@ final class SheetManager $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId); + $mergeCells = []; + if ($this->options->SHOULD_LOAD_MERGE_CELLS) { + $mergeCells = (new SheetMergeCellsReader( + $this->filePath, + $sheetDataXMLFilePath, + $xmlReader = new XMLReader(), + new XMLProcessor($xmlReader) + ))->getMergeCells(); + } + return new Sheet( $this->createRowIterator($this->filePath, $sheetDataXMLFilePath, $this->options, $this->sharedStringsManager), $this->createSheetHeaderReader($this->filePath, $sheetDataXMLFilePath), $sheetIndexZeroBased, $sheetName, $isSheetActive, - $isSheetVisible + $isSheetVisible, + $mergeCells ); } @@ -240,8 +253,6 @@ final class SheetManager Options $options, SharedStringsManager $sharedStringsManager ): RowIterator { - $xmlReader = new XMLReader(); - $workbookRelationshipsManager = new WorkbookRelationshipsManager($filePath); $styleManager = new StyleManager( $filePath, @@ -262,7 +273,7 @@ final class SheetManager $filePath, $sheetDataXMLFilePath, $options->SHOULD_PRESERVE_EMPTY_ROWS, - $xmlReader, + $xmlReader = new XMLReader(), new XMLProcessor($xmlReader), $cellValueFormatter, new RowManager() diff --git a/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php b/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php index caac7dbcd86..bf3c88f199d 100644 --- a/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php +++ b/lib/openspout/src/Reader/XLSX/Manager/StyleManager.php @@ -67,8 +67,7 @@ class StyleManager implements StyleManagerInterface private array $numFmtIdToIsDateFormatCache = []; /** - * @param string $filePath Path of the XLSX file being read - * @param ?string $stylesXMLFilePath + * @param string $filePath Path of the XLSX file being read */ public function __construct(string $filePath, ?string $stylesXMLFilePath) { @@ -98,7 +97,16 @@ class StyleManager implements StyleManagerInterface public function getNumberFormatCode(int $styleId): string { + if (null === $this->stylesXMLFilePath) { + return ''; + } + $stylesAttributes = $this->getStylesAttributes(); + + if (!isset($stylesAttributes[$styleId])) { + return ''; + } + $styleAttributes = $stylesAttributes[$styleId]; $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; \assert(\is_int($numFmtId)); diff --git a/lib/openspout/src/Reader/XLSX/Options.php b/lib/openspout/src/Reader/XLSX/Options.php index 1c8331f827a..636d2a4c1ad 100644 --- a/lib/openspout/src/Reader/XLSX/Options.php +++ b/lib/openspout/src/Reader/XLSX/Options.php @@ -13,4 +13,5 @@ final class Options public bool $SHOULD_FORMAT_DATES = false; public bool $SHOULD_PRESERVE_EMPTY_ROWS = false; public bool $SHOULD_USE_1904_DATES = false; + public bool $SHOULD_LOAD_MERGE_CELLS = false; } diff --git a/lib/openspout/src/Reader/XLSX/RowIterator.php b/lib/openspout/src/Reader/XLSX/RowIterator.php index b3ad10e84c5..a7ce9a0263a 100644 --- a/lib/openspout/src/Reader/XLSX/RowIterator.php +++ b/lib/openspout/src/Reader/XLSX/RowIterator.php @@ -47,8 +47,8 @@ final class RowIterator implements RowIteratorInterface /** @var XMLProcessor Helper Object to process XML nodes */ private readonly XMLProcessor $xmlProcessor; - /** @var Helper\CellValueFormatter Helper to format cell values */ - private readonly Helper\CellValueFormatter $cellValueFormatter; + /** @var CellValueFormatter Helper to format cell values */ + private readonly CellValueFormatter $cellValueFormatter; /** @var RowManager Manages rows */ private readonly RowManager $rowManager; diff --git a/lib/openspout/src/Reader/XLSX/Sheet.php b/lib/openspout/src/Reader/XLSX/Sheet.php index 504b20ed33a..4b59f36a4b3 100644 --- a/lib/openspout/src/Reader/XLSX/Sheet.php +++ b/lib/openspout/src/Reader/XLSX/Sheet.php @@ -5,46 +5,60 @@ declare(strict_types=1); namespace OpenSpout\Reader\XLSX; use OpenSpout\Reader\Common\ColumnWidth; +use OpenSpout\Reader\SheetWithMergeCellsInterface; use OpenSpout\Reader\SheetWithVisibilityInterface; /** * @implements SheetWithVisibilityInterface + * @implements SheetWithMergeCellsInterface */ -final class Sheet implements SheetWithVisibilityInterface +final readonly class Sheet implements SheetWithVisibilityInterface, SheetWithMergeCellsInterface { /** @var RowIterator To iterate over sheet's rows */ - private readonly RowIterator $rowIterator; + private RowIterator $rowIterator; /** @var SheetHeaderReader To read the header of the sheet, containing for instance the col widths */ - private readonly SheetHeaderReader $headerReader; + private SheetHeaderReader $headerReader; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ - private readonly int $index; + private int $index; /** @var string Name of the sheet */ - private readonly string $name; + private string $name; /** @var bool Whether the sheet was the active one */ - private readonly bool $isActive; + private bool $isActive; /** @var bool Whether the sheet is visible */ - private readonly bool $isVisible; + private bool $isVisible; + + /** @var list Merge cells list ["C7:E7", "A9:D10"] */ + private array $mergeCells; /** - * @param RowIterator $rowIterator The corresponding row iterator - * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) - * @param string $sheetName Name of the sheet - * @param bool $isSheetActive Whether the sheet was defined as active - * @param bool $isSheetVisible Whether the sheet is visible + * @param RowIterator $rowIterator The corresponding row iterator + * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) + * @param string $sheetName Name of the sheet + * @param bool $isSheetActive Whether the sheet was defined as active + * @param bool $isSheetVisible Whether the sheet is visible + * @param list $mergeCells Merge cells list ["C7:E7", "A9:D10"] */ - public function __construct(RowIterator $rowIterator, SheetHeaderReader $headerReader, int $sheetIndex, string $sheetName, bool $isSheetActive, bool $isSheetVisible) - { + public function __construct( + RowIterator $rowIterator, + SheetHeaderReader $headerReader, + int $sheetIndex, + string $sheetName, + bool $isSheetActive, + bool $isSheetVisible, + array $mergeCells + ) { $this->rowIterator = $rowIterator; $this->headerReader = $headerReader; $this->index = $sheetIndex; $this->name = $sheetName; $this->isActive = $isSheetActive; $this->isVisible = $isSheetVisible; + $this->mergeCells = $mergeCells; } public function getRowIterator(): RowIterator @@ -91,4 +105,12 @@ final class Sheet implements SheetWithVisibilityInterface { return $this->isVisible; } + + /** + * @return list Merge cells list ["C7:E7", "A9:D10"] + */ + public function getMergeCells(): array + { + return $this->mergeCells; + } } diff --git a/lib/openspout/src/Reader/XLSX/SheetMergeCellsReader.php b/lib/openspout/src/Reader/XLSX/SheetMergeCellsReader.php new file mode 100644 index 00000000000..e5ac387b190 --- /dev/null +++ b/lib/openspout/src/Reader/XLSX/SheetMergeCellsReader.php @@ -0,0 +1,69 @@ + Merged cells list */ + private array $mergeCells = []; + + /** + * @param string $filePath Path of the XLSX file being read + * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml + * @param XMLProcessor $xmlProcessor Helper to process XML files + */ + public function __construct( + string $filePath, + string $sheetDataXMLFilePath, + XMLReader $xmlReader, + XMLProcessor $xmlProcessor + ) { + $sheetDataXMLFilePath = ltrim($sheetDataXMLFilePath, '/'); + + // Register all callbacks to process different nodes when reading the XML file + $xmlProcessor->registerCallback(self::XML_NODE_MERGE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processMergeCellsStartingNode']); + $xmlReader->close(); + + if (false === $xmlReader->openFileInZip($filePath, $sheetDataXMLFilePath)) { + throw new IOException("Could not open \"{$sheetDataXMLFilePath}\"."); + } + + // Now read the entire header of the sheet, until we reach the element + $xmlProcessor->readUntilStopped(); + $xmlReader->close(); + } + + /** + * @return list + */ + public function getMergeCells(): array + { + return $this->mergeCells; + } + + /** + * @param XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * + * @return int A return code that indicates what action should the processor take next + */ + private function processMergeCellsStartingNode(XMLReader $xmlReader): int + { + $this->mergeCells[] = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); + + return XMLProcessor::PROCESSING_CONTINUE; + } +} diff --git a/lib/openspout/src/Writer/AbstractWriter.php b/lib/openspout/src/Writer/AbstractWriter.php index eef34fb4c55..f983a661a58 100644 --- a/lib/openspout/src/Writer/AbstractWriter.php +++ b/lib/openspout/src/Writer/AbstractWriter.php @@ -13,9 +13,6 @@ abstract class AbstractWriter implements WriterInterface /** @var resource Pointer to the file/stream we will write to */ protected $filePointer; - /** @var string document creator */ - protected string $creator = 'OpenSpout'; - /** @var string Content-Type value for the header - to be defined by child class */ protected static string $headerContentType; @@ -122,11 +119,6 @@ abstract class AbstractWriter implements WriterInterface } } - final public function setCreator(string $creator): void - { - $this->creator = $creator; - } - final public function getWrittenRowCount(): int { return $this->writtenRowCount; diff --git a/lib/openspout/src/Writer/AutoFilter.php b/lib/openspout/src/Writer/AutoFilter.php index 5fefac1068b..76822532cb6 100644 --- a/lib/openspout/src/Writer/AutoFilter.php +++ b/lib/openspout/src/Writer/AutoFilter.php @@ -4,10 +4,7 @@ declare(strict_types=1); namespace OpenSpout\Writer; -/** - * @readonly - */ -final class AutoFilter +final readonly class AutoFilter { /** * @param 0|positive-int $fromColumnIndex @@ -16,9 +13,9 @@ final class AutoFilter * @param positive-int $toRow */ public function __construct( - public readonly int $fromColumnIndex, - public readonly int $fromRow, - public readonly int $toColumnIndex, - public readonly int $toRow + public int $fromColumnIndex, + public int $fromRow, + public int $toColumnIndex, + public int $toRow ) {} } diff --git a/lib/openspout/src/Writer/CSV/Writer.php b/lib/openspout/src/Writer/CSV/Writer.php index f1087eac780..9f69c59cc6c 100644 --- a/lib/openspout/src/Writer/CSV/Writer.php +++ b/lib/openspout/src/Writer/CSV/Writer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace OpenSpout\Writer\CSV; +use Exception; use OpenSpout\Common\Entity\Cell; use OpenSpout\Common\Entity\Row; use OpenSpout\Common\Exception\IOException; @@ -29,6 +30,11 @@ final class Writer extends AbstractWriter return $this->options; } + public function setCreator(string $creator): void + { + throw new Exception('Method unsopported for CSV documents'); + } + /** * Opens the CSV streamer and makes it ready to accept data. */ diff --git a/lib/openspout/src/Writer/Common/ColumnWidth.php b/lib/openspout/src/Writer/Common/ColumnWidth.php index c24fbcb8d07..2d771c7cd3b 100644 --- a/lib/openspout/src/Writer/Common/ColumnWidth.php +++ b/lib/openspout/src/Writer/Common/ColumnWidth.php @@ -7,15 +7,15 @@ namespace OpenSpout\Writer\Common; /** * @internal */ -final class ColumnWidth +final readonly class ColumnWidth { /** * @param positive-int $start * @param positive-int $end */ public function __construct( - public readonly int $start, - public readonly int $end, - public readonly float $width, + public int $start, + public int $end, + public float $width, ) {} } diff --git a/lib/openspout/src/Writer/Common/Entity/Sheet.php b/lib/openspout/src/Writer/Common/Entity/Sheet.php index fba9f305177..f5ce8f0e468 100644 --- a/lib/openspout/src/Writer/Common/Entity/Sheet.php +++ b/lib/openspout/src/Writer/Common/Entity/Sheet.php @@ -9,6 +9,7 @@ use OpenSpout\Writer\Common\ColumnWidth; use OpenSpout\Writer\Common\Manager\SheetManager; use OpenSpout\Writer\Exception\InvalidSheetNameException; use OpenSpout\Writer\XLSX\Entity\SheetView; +use OpenSpout\Writer\XLSX\Options\SheetProtection; /** * External representation of a worksheet. @@ -45,6 +46,8 @@ final class Sheet /** @var string rows to repeat at top */ private ?string $printTitleRows = null; + private ?SheetProtection $sheetProtection = null; + /** * @param 0|positive-int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $associatedWorkbookId ID of the sheet's associated workbook @@ -219,4 +222,19 @@ final class Sheet { $this->printTitleRows = $printTitleRows; } + + /** + * @return $this + */ + public function setSheetProtection(SheetProtection $sheetProtection): self + { + $this->sheetProtection = $sheetProtection; + + return $this; + } + + public function getSheetProtection(): ?SheetProtection + { + return $this->sheetProtection; + } } diff --git a/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php b/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php index ff7fe3803e0..7178de7ff79 100644 --- a/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php +++ b/lib/openspout/src/Writer/Common/Manager/RegisteredStyle.php @@ -11,11 +11,11 @@ use OpenSpout\Common\Entity\Style\Style; * * @internal */ -final class RegisteredStyle +final readonly class RegisteredStyle { - private readonly Style $style; + private Style $style; - private readonly bool $isMatchingRowStyle; + private bool $isMatchingRowStyle; public function __construct(Style $style, bool $isMatchingRowStyle) { diff --git a/lib/openspout/src/Writer/Common/Manager/SheetManager.php b/lib/openspout/src/Writer/Common/Manager/SheetManager.php index 2fe54df60d8..9a355559d69 100644 --- a/lib/openspout/src/Writer/Common/Manager/SheetManager.php +++ b/lib/openspout/src/Writer/Common/Manager/SheetManager.php @@ -60,7 +60,7 @@ final class SheetManager } if ($this->doesContainInvalidCharacters($name)) { - $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]'; + $failedRequirements[] = 'It should not contain these characters: \ / ? * : [ or ]'; } if ($this->doesStartOrEndWithSingleQuote($name)) { diff --git a/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php b/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php index ac6ae85851d..94546718cc6 100644 --- a/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php +++ b/lib/openspout/src/Writer/Common/Manager/Style/PossiblyUpdatedStyle.php @@ -9,10 +9,10 @@ use OpenSpout\Common\Entity\Style\Style; /** * @internal */ -final class PossiblyUpdatedStyle +final readonly class PossiblyUpdatedStyle { - private readonly Style $style; - private readonly bool $isUpdated; + private Style $style; + private bool $isUpdated; public function __construct(Style $style, bool $isUpdated) { diff --git a/lib/openspout/src/Writer/Common/Manager/Style/StyleMerger.php b/lib/openspout/src/Writer/Common/Manager/Style/StyleMerger.php index eb9fe2444f2..e6478808849 100644 --- a/lib/openspout/src/Writer/Common/Manager/Style/StyleMerger.php +++ b/lib/openspout/src/Writer/Common/Manager/Style/StyleMerger.php @@ -75,6 +75,9 @@ final class StyleMerger if (!$style->hasSetWrapText() && $baseStyle->hasSetWrapText()) { $styleToUpdate->setShouldWrapText($baseStyle->shouldWrapText()); } + if (!$style->hasSetTextRotation() && $baseStyle->hasSetTextRotation()) { + $styleToUpdate->setTextRotation($baseStyle->textRotation()); + } if (!$style->hasSetShrinkToFit() && $baseStyle->shouldShrinkToFit()) { $styleToUpdate->setShouldShrinkToFit(); } diff --git a/lib/openspout/src/Writer/Exception/Border/InvalidNameException.php b/lib/openspout/src/Writer/Exception/Border/InvalidNameException.php index 67897ab7e3a..c1a548d2a9f 100644 --- a/lib/openspout/src/Writer/Exception/Border/InvalidNameException.php +++ b/lib/openspout/src/Writer/Exception/Border/InvalidNameException.php @@ -13,6 +13,6 @@ final class InvalidNameException extends WriterException { $msg = '%s is not a valid name identifier for a border. Valid identifiers are: %s.'; - parent::__construct(sprintf($msg, $name, implode(',', BorderPart::allowedNames))); + parent::__construct(\sprintf($msg, $name, implode(',', BorderPart::allowedNames))); } } diff --git a/lib/openspout/src/Writer/Exception/Border/InvalidStyleException.php b/lib/openspout/src/Writer/Exception/Border/InvalidStyleException.php index ab03b20a24e..bb0b7e55eab 100644 --- a/lib/openspout/src/Writer/Exception/Border/InvalidStyleException.php +++ b/lib/openspout/src/Writer/Exception/Border/InvalidStyleException.php @@ -13,6 +13,6 @@ final class InvalidStyleException extends WriterException { $msg = '%s is not a valid style identifier for a border. Valid identifiers are: %s.'; - parent::__construct(sprintf($msg, $name, implode(',', BorderPart::allowedStyles))); + parent::__construct(\sprintf($msg, $name, implode(',', BorderPart::allowedStyles))); } } diff --git a/lib/openspout/src/Writer/Exception/Border/InvalidWidthException.php b/lib/openspout/src/Writer/Exception/Border/InvalidWidthException.php index 43d4021b0a6..60a67ffc862 100644 --- a/lib/openspout/src/Writer/Exception/Border/InvalidWidthException.php +++ b/lib/openspout/src/Writer/Exception/Border/InvalidWidthException.php @@ -13,6 +13,6 @@ final class InvalidWidthException extends WriterException { $msg = '%s is not a valid width identifier for a border. Valid identifiers are: %s.'; - parent::__construct(sprintf($msg, $name, implode(',', BorderPart::allowedWidths))); + parent::__construct(\sprintf($msg, $name, implode(',', BorderPart::allowedWidths))); } } diff --git a/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php b/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php index f043f762079..d87a8c62e67 100644 --- a/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php +++ b/lib/openspout/src/Writer/ODS/Helper/BorderHelper.php @@ -47,14 +47,14 @@ final class BorderHelper $definition = 'fo:border-%s="%s"'; if (Border::STYLE_NONE === $borderPart->getStyle()) { - $borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none'); + $borderPartDefinition = \sprintf($definition, $borderPart->getName(), 'none'); } else { $attributes = [ self::widthMap[$borderPart->getWidth()], self::styleMap[$borderPart->getStyle()], '#'.$borderPart->getColor(), ]; - $borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes)); + $borderPartDefinition = \sprintf($definition, $borderPart->getName(), implode(' ', $attributes)); } return $borderPartDefinition; diff --git a/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php b/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php index df9c0e3929a..63402e54420 100644 --- a/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php +++ b/lib/openspout/src/Writer/ODS/Helper/FileSystemHelper.php @@ -28,11 +28,11 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface public const MIMETYPE_FILE_NAME = 'mimetype'; public const STYLES_XML_FILE_NAME = 'styles.xml'; - private readonly string $baseFolderRealPath; + private string $baseFolderRealPath; /** @var string document creator */ - private readonly string $creator; - private readonly CommonFileSystemHelper $baseFileSystemHelper; + private string $creator; + private CommonFileSystemHelper $baseFileSystemHelper; /** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */ private string $rootFolder; diff --git a/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php b/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php index ca516d6ea95..dc0af0bddbe 100644 --- a/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php +++ b/lib/openspout/src/Writer/ODS/Manager/Style/StyleManager.php @@ -336,7 +336,7 @@ final class StyleManager extends CommonStyleManager return ''; } - return sprintf( + return \sprintf( ' fo:text-align="%s" ', $this->transformCellAlignment($style->getCellAlignment()) ); @@ -351,7 +351,7 @@ final class StyleManager extends CommonStyleManager return ''; } - return sprintf( + return \sprintf( ' fo:vertical-align="%s" ', $this->transformCellVerticalAlignment($style->getCellVerticalAlignment()) ); @@ -423,7 +423,7 @@ final class StyleManager extends CommonStyleManager return BorderHelper::serializeBorderPart($borderPart); }, $border->getParts()); - return sprintf(' %s ', implode(' ', $borders)); + return \sprintf(' %s ', implode(' ', $borders)); } /** @@ -431,6 +431,6 @@ final class StyleManager extends CommonStyleManager */ private function getBackgroundColorXMLContent(string $bgColor): string { - return sprintf(' fo:background-color="#%s" ', $bgColor); + return \sprintf(' fo:background-color="#%s" ', $bgColor); } } diff --git a/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php b/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php index 0c548fd21f4..19566ab8c19 100644 --- a/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php +++ b/lib/openspout/src/Writer/ODS/Manager/WorksheetManager.php @@ -22,16 +22,16 @@ use OpenSpout\Writer\ODS\Manager\Style\StyleManager; /** * @internal */ -final class WorksheetManager implements WorksheetManagerInterface +final readonly class WorksheetManager implements WorksheetManagerInterface { /** @var ODSEscaper Strings escaper */ - private readonly ODSEscaper $stringsEscaper; + private ODSEscaper $stringsEscaper; /** @var StyleManager Manages styles */ - private readonly StyleManager $styleManager; + private StyleManager $styleManager; /** @var StyleMerger Helper to merge styles together */ - private readonly StyleMerger $styleMerger; + private StyleMerger $styleMerger; /** * WorksheetManager constructor. @@ -88,7 +88,7 @@ final class WorksheetManager implements WorksheetManagerInterface $databaseRange = ''; if (null !== $autofilter = $externalSheet->getAutoFilter()) { - $rangeAddress = sprintf( + $rangeAddress = \sprintf( '\'%s\'.%s%s:\'%s\'.%s%s', $escapedSheetName, CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), diff --git a/lib/openspout/src/Writer/ODS/Writer.php b/lib/openspout/src/Writer/ODS/Writer.php index fbbb56c07b8..2eb4b04993b 100644 --- a/lib/openspout/src/Writer/ODS/Writer.php +++ b/lib/openspout/src/Writer/ODS/Writer.php @@ -19,6 +19,9 @@ final class Writer extends AbstractWriterMultiSheets { /** @var string Content-Type value for the header */ protected static string $headerContentType = 'application/vnd.oasis.opendocument.spreadsheet'; + + /** @var string document creator */ + protected string $creator = 'OpenSpout'; private readonly Options $options; public function __construct(?Options $options = null) @@ -31,6 +34,11 @@ final class Writer extends AbstractWriterMultiSheets return $this->options; } + public function setCreator(string $creator): void + { + $this->creator = $creator; + } + protected function createWorkbookManager(): WorkbookManager { $workbook = new Workbook(); diff --git a/lib/openspout/src/Writer/WriterInterface.php b/lib/openspout/src/Writer/WriterInterface.php index 272eba96143..97131866499 100644 --- a/lib/openspout/src/Writer/WriterInterface.php +++ b/lib/openspout/src/Writer/WriterInterface.php @@ -40,6 +40,13 @@ interface WriterInterface */ public function addRow(Row $row): void; + /** + * Set document creator. + * + * @param string $creator document creator + */ + public function setCreator(string $creator): void; + /** * Appends the rows to the end of the stream. * @@ -51,13 +58,6 @@ interface WriterInterface */ public function addRows(array $rows): void; - /** - * Set document creator. - * - * @param string $creator document creator - */ - public function setCreator(string $creator): void; - /** * @return 0|positive-int */ diff --git a/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php b/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php index dff73459f12..c2ce1d8d70f 100644 --- a/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php +++ b/lib/openspout/src/Writer/XLSX/Helper/BorderHelper.php @@ -48,8 +48,8 @@ final class BorderHelper $borderStyle = self::getBorderStyle($borderPart); - $colorEl = sprintf('', $borderPart->getColor()); - $partEl = sprintf( + $colorEl = \sprintf('', $borderPart->getColor()); + $partEl = \sprintf( '<%s style="%s">%s', $borderPart->getName(), $borderStyle, diff --git a/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php b/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php index b39bdb6e71e..0ed8db0a982 100644 --- a/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php +++ b/lib/openspout/src/Writer/XLSX/Helper/FileSystemHelper.php @@ -16,6 +16,7 @@ use OpenSpout\Writer\Common\Helper\ZipHelper; use OpenSpout\Writer\XLSX\Manager\Style\StyleManager; use OpenSpout\Writer\XLSX\MergeCell; use OpenSpout\Writer\XLSX\Options; +use OpenSpout\Writer\XLSX\Properties; /** * @internal @@ -31,6 +32,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface public const RELS_FILE_NAME = '.rels'; public const APP_XML_FILE_NAME = 'app.xml'; public const CORE_XML_FILE_NAME = 'core.xml'; + public const CUSTOM_XML_FILE_NAME = 'custom.xml'; public const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml'; public const WORKBOOK_XML_FILE_NAME = 'workbook.xml'; public const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels'; @@ -47,8 +49,8 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface /** @var ZipHelper Helper to perform tasks with Zip archive */ private readonly ZipHelper $zipHelper; - /** @var string document creator */ - private readonly string $creator; + /** @var Properties document properties */ + private readonly Properties $properties; /** @var XLSX Used to escape XML data */ private readonly XLSX $escaper; @@ -75,18 +77,18 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface private string $sheetsContentTempFolder; /** - * @param string $baseFolderPath The path of the base folder where all the I/O can occur - * @param ZipHelper $zipHelper Helper to perform tasks with Zip archive - * @param XLSX $escaper Used to escape XML data - * @param string $creator document creator + * @param string $baseFolderPath The path of the base folder where all the I/O can occur + * @param ZipHelper $zipHelper Helper to perform tasks with Zip archive + * @param XLSX $escaper Used to escape XML data + * @param Properties $properties document properies */ - public function __construct(string $baseFolderPath, ZipHelper $zipHelper, XLSX $escaper, string $creator) + public function __construct(string $baseFolderPath, ZipHelper $zipHelper, XLSX $escaper, Properties $properties) { $this->baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath); $this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath(); $this->zipHelper = $zipHelper; $this->escaper = $escaper; - $this->creator = $creator; + $this->properties = $properties; } public function createFolder(string $parentFolderPath, string $folderName): string @@ -168,10 +170,19 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface } $contentTypesXmlFileContents .= <<<'EOD' - - - - + + + + + EOD; + + if ([] !== $this->properties->customProperties) { + $contentTypesXmlFileContents .= <<<'EOD' + + EOD; + } + + $contentTypesXmlFileContents .= <<<'EOD' EOD; @@ -185,11 +196,18 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface * * @param Worksheet[] $worksheets */ - public function createWorkbookFile(array $worksheets): self + public function createWorkbookFile(Options $options, array $worksheets): self { $workbookXmlFileContents = <<<'EOD' + EOD; + + if (null !== $options->getWorkbookProtection()) { + $workbookXmlFileContents .= $options->getWorkbookProtection()->getXml(); + } + + $workbookXmlFileContents .= <<<'EOD' EOD; @@ -212,7 +230,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface $sheet = $worksheet->getExternalSheet(); if (null !== $autofilter = $sheet->getAutoFilter()) { $worksheetName = $sheet->getName(); - $name = sprintf( + $name = \sprintf( '\'%s\'!$%s$%s:$%s$%s', $this->escaper->escape($worksheetName), CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), @@ -321,24 +339,23 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface fwrite($worksheetFilePointer, self::SHEET_XML_FILE_HEADER); // AutoFilter tags - $range = ''; if (null !== $autofilter = $sheet->getAutoFilter()) { - $range = sprintf( - '%s%s:%s%s', - CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), - $autofilter->fromRow, - CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex), - $autofilter->toRow - ); if (isset($pageSetup) && $pageSetup->fitToPage) { fwrite($worksheetFilePointer, ''); } else { fwrite($worksheetFilePointer, ''); } - fwrite($worksheetFilePointer, sprintf('', $range)); } elseif (isset($pageSetup) && $pageSetup->fitToPage) { fwrite($worksheetFilePointer, ''); } + $sheetRange = \sprintf( + '%s%s:%s%s', + CellHelper::getColumnLettersFromColumnIndex(0), + 1, + CellHelper::getColumnLettersFromColumnIndex($worksheet->getMaxNumColumns() - 1), + $worksheet->getLastWrittenRowIndex() + ); + fwrite($worksheetFilePointer, \sprintf('', $sheetRange)); if (null !== ($sheetView = $sheet->getSheetView())) { fwrite($worksheetFilePointer, ''.$sheetView->getXml().''); } @@ -350,9 +367,20 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface $this->copyFileContentsToTarget($worksheetFilePath, $worksheetFilePointer); fwrite($worksheetFilePointer, ''); + if (null !== $sheet->getSheetProtection()) { + fwrite($worksheetFilePointer, $sheet->getSheetProtection()->getXml()); + } + // AutoFilter tag - if ('' !== $range) { - fwrite($worksheetFilePointer, sprintf('', $range)); + if (null !== $autofilter) { + $autoFilterRange = \sprintf( + '%s%s:%s%s', + CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex), + $autofilter->fromRow, + CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex), + $autofilter->toRow + ); + fwrite($worksheetFilePointer, \sprintf('', $autoFilterRange)); } // create nodes for merge cells @@ -365,7 +393,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface foreach ($mergeCells as $mergeCell) { $topLeft = CellHelper::getColumnLettersFromColumnIndex($mergeCell->topLeftColumn).$mergeCell->topLeftRow; $bottomRight = CellHelper::getColumnLettersFromColumnIndex($mergeCell->bottomRightColumn).$mergeCell->bottomRightRow; - $mergeCellString .= sprintf( + $mergeCellString .= \sprintf( '', $topLeft, $bottomRight @@ -585,13 +613,21 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface */ private function createRelsFile(): self { - $relsFileContents = <<<'EOD' + $relationshipsXmlContents = <<<'EOD' + + + + EOD; + + if ([] !== $this->properties->customProperties) { + $relationshipsXmlContents .= <<<'EOD' + + EOD; + } + + $relsFileContents = << - - - - - + {$relationshipsXmlContents} EOD; $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents); @@ -611,6 +647,10 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface $this->createAppXmlFile(); $this->createCoreXmlFile(); + if ([] !== $this->properties->customProperties) { + $this->createCustomXmlFile(); + } + return $this; } @@ -624,7 +664,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface $appXmlFileContents = << - {$this->creator} + {$this->properties->application} 0 EOD; @@ -645,6 +685,14 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface $coreXmlFileContents = << + {$this->properties->title} + {$this->properties->subject} + {$this->properties->creator} + {$this->properties->lastModifiedBy} + {$this->properties->keywords} + {$this->properties->description} + {$this->properties->category} + {$this->properties->language} {$createdDate} {$createdDate} 0 @@ -656,6 +704,35 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface return $this; } + /** + * Creates the "custom.xml" file under the "docProps" folder. + * + * @throws IOException If unable to create the file + */ + private function createCustomXmlFile(): self + { + /** The pid must increment for each property, starting with 2 */ + $pid = 2; + $propertiesXmlContents = ''; + + foreach ($this->properties->customProperties as $name => $value) { + $propertiesXmlContents .= <<{$value} + EOD; + + ++$pid; + } + + $customXmlFileContents = << + {$propertiesXmlContents} + EOD; + + $this->createFileWithContents($this->docPropsFolder, self::CUSTOM_XML_FILE_NAME, $customXmlFileContents); + + return $this; + } + /** * Creates the "xl" folder under the root folder as well as its subfolders. * diff --git a/lib/openspout/src/Writer/XLSX/Helper/PasswordHashHelper.php b/lib/openspout/src/Writer/XLSX/Helper/PasswordHashHelper.php new file mode 100644 index 00000000000..2872792011d --- /dev/null +++ b/lib/openspout/src/Writer/XLSX/Helper/PasswordHashHelper.php @@ -0,0 +1,30 @@ += 0; --$i) { + $intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1; + $intermediate2 = 2 * $verifier; + $intermediate2 &= 0x7FFF; + $intermediate3 = $intermediate1 | $intermediate2; + $verifier = $intermediate3 ^ \ord($passwordArray[$i]); + } + + $verifier ^= 0xCE4B; + + return strtoupper(dechex($verifier)); + } +} diff --git a/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php b/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php index 8f472ebbdcb..c47c45efd91 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/SharedStringsManager.php @@ -79,7 +79,7 @@ final class SharedStringsManager // Adding 1 to take into account the space between the last xml attribute and "count" fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1); - fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="'.$this->numSharedStrings.'" uniqueCount="'.$this->numSharedStrings.'"')); + fwrite($this->sharedStringsFilePointer, \sprintf("%-{$defaultStringsCountPartLength}s", 'count="'.$this->numSharedStrings.'" uniqueCount="'.$this->numSharedStrings.'"')); fclose($this->sharedStringsFilePointer); } diff --git a/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php b/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php index 3953f4dc5f8..1ea8add1e04 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/Style/StyleManager.php @@ -7,6 +7,7 @@ namespace OpenSpout\Writer\XLSX\Manager\Style; use OpenSpout\Common\Entity\Style\BorderPart; use OpenSpout\Common\Entity\Style\Color; use OpenSpout\Common\Entity\Style\Style; +use OpenSpout\Common\Helper\Escaper\XLSX as XLSXEscaper; use OpenSpout\Writer\Common\Manager\Style\AbstractStyleManager as CommonStyleManager; use OpenSpout\Writer\XLSX\Helper\BorderHelper; @@ -17,9 +18,13 @@ use OpenSpout\Writer\XLSX\Helper\BorderHelper; */ final class StyleManager extends CommonStyleManager { - public function __construct(StyleRegistry $styleRegistry) + /** @var XLSXEscaper Strings escaper */ + private readonly XLSXEscaper $stringsEscaper; + + public function __construct(StyleRegistry $styleRegistry, XLSXEscaper $stringsEscaper) { parent::__construct($styleRegistry); + $this->stringsEscaper = $stringsEscaper; } /** @@ -91,7 +96,7 @@ final class StyleManager extends CommonStyleManager /** @var Style $style */ $style = $this->styleRegistry->getStyleFromStyleId($styleId); $format = $style->getFormat(); - $tags[] = ''; + $tags[] = ''; } $content = ''; $content .= implode('', $tags); @@ -147,7 +152,7 @@ final class StyleManager extends CommonStyleManager // Excel reserves two default fills $fillsCount = \count($registeredFills) + 2; - $content = sprintf('', $fillsCount); + $content = \sprintf('', $fillsCount); $content .= ''; $content .= ''; @@ -158,7 +163,7 @@ final class StyleManager extends CommonStyleManager $style = $this->styleRegistry->getStyleFromStyleId($styleId); $backgroundColor = $style->getBackgroundColor(); - $content .= sprintf( + $content .= \sprintf( '', $backgroundColor ); @@ -236,16 +241,16 @@ final class StyleManager extends CommonStyleManager $content .= ' applyFont="1"'; } - $content .= sprintf(' applyBorder="%d"', (bool) $style->getBorder()); + $content .= \sprintf(' applyBorder="%d"', (bool) $style->getBorder()); - if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit()) { + if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit() || $style->hasSetTextRotation()) { $content .= ' applyAlignment="1">'; $content .= 'shouldApplyCellAlignment()) { - $content .= sprintf(' horizontal="%s"', $style->getCellAlignment()); + $content .= \sprintf(' horizontal="%s"', $style->getCellAlignment()); } if ($style->shouldApplyCellVerticalAlignment()) { - $content .= sprintf(' vertical="%s"', $style->getCellVerticalAlignment()); + $content .= \sprintf(' vertical="%s"', $style->getCellVerticalAlignment()); } if ($style->hasSetWrapText()) { $content .= ' wrapText="'.($style->shouldWrapText() ? '1' : '0').'"'; @@ -253,6 +258,9 @@ final class StyleManager extends CommonStyleManager if ($style->shouldShrinkToFit()) { $content .= ' shrinkToFit="true"'; } + if ($style->hasSetTextRotation()) { + $content .= \sprintf(' textRotation="%s"', $style->textRotation()); + } $content .= '/>'; $content .= ''; diff --git a/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php b/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php index 082951753f1..5a625fcd258 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/WorkbookManager.php @@ -75,7 +75,7 @@ final class WorkbookManager extends AbstractWorkbookManager ->createContentFiles($this->options, $worksheets) ->deleteWorksheetTempFolder() ->createContentTypesFile($worksheets) - ->createWorkbookFile($worksheets) + ->createWorkbookFile($this->options, $worksheets) ->createWorkbookRelsFile($worksheets) ->createWorksheetRelsFiles($worksheets) ->createStylesFile($this->styleManager) diff --git a/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php b/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php index 94ca09c87e9..e45e9101f5d 100644 --- a/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php +++ b/lib/openspout/src/Writer/XLSX/Manager/WorksheetManager.php @@ -24,7 +24,7 @@ use OpenSpout\Writer\XLSX\Options; /** * @internal */ -final class WorksheetManager implements WorksheetManagerInterface +final readonly class WorksheetManager implements WorksheetManagerInterface { /** * Maximum number of characters a cell can contain. @@ -36,24 +36,24 @@ final class WorksheetManager implements WorksheetManagerInterface public const MAX_CHARACTERS_PER_CELL = 32767; /** @var CommentsManager Manages comments */ - private readonly CommentsManager $commentsManager; + private CommentsManager $commentsManager; - private readonly Options $options; + private Options $options; /** @var StyleManager Manages styles */ - private readonly StyleManager $styleManager; + private StyleManager $styleManager; /** @var StyleMerger Helper to merge styles together */ - private readonly StyleMerger $styleMerger; + private StyleMerger $styleMerger; /** @var SharedStringsManager Helper to write shared strings */ - private readonly SharedStringsManager $sharedStringsManager; + private SharedStringsManager $sharedStringsManager; /** @var XLSXEscaper Strings escaper */ - private readonly XLSXEscaper $stringsEscaper; + private XLSXEscaper $stringsEscaper; /** @var StringHelper String helper */ - private readonly StringHelper $stringHelper; + private StringHelper $stringHelper; /** * WorksheetManager constructor. @@ -198,14 +198,14 @@ final class WorksheetManager implements WorksheetManagerInterface } elseif ($cell instanceof Cell\NumericCell) { $cellXML .= '>'.$cell->getValue().''; } elseif ($cell instanceof Cell\FormulaCell) { - $cellXML .= '>'.substr($cell->getValue(), 1).''; + $cellXML .= '>'.$this->stringsEscaper->escape(substr($cell->getValue(), 1)).''; } elseif ($cell instanceof Cell\DateTimeCell) { $cellXML .= '>'.DateHelper::toExcel($cell->getValue()).''; } elseif ($cell instanceof Cell\DateIntervalCell) { $cellXML .= '>'.DateIntervalHelper::toExcel($cell->getValue()).''; } elseif ($cell instanceof Cell\ErrorCell) { // only writes the error value if it's a string - $cellXML .= ' t="e">'.$cell->getRawValue().''; + $cellXML .= ' t="e">'.$this->stringsEscaper->escape($cell->getRawValue()).''; } elseif ($cell instanceof Cell\EmptyCell) { if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) { $cellXML .= '/>'; diff --git a/lib/openspout/src/Writer/XLSX/MergeCell.php b/lib/openspout/src/Writer/XLSX/MergeCell.php index 02e23438bc7..1d8cd889e2d 100644 --- a/lib/openspout/src/Writer/XLSX/MergeCell.php +++ b/lib/openspout/src/Writer/XLSX/MergeCell.php @@ -7,7 +7,7 @@ namespace OpenSpout\Writer\XLSX; /** * @internal */ -final class MergeCell +final readonly class MergeCell { /** * @param 0|positive-int $sheetIndex @@ -17,10 +17,10 @@ final class MergeCell * @param positive-int $bottomRightRow */ public function __construct( - public readonly int $sheetIndex, - public readonly int $topLeftColumn, - public readonly int $topLeftRow, - public readonly int $bottomRightColumn, - public readonly int $bottomRightRow, + public int $sheetIndex, + public int $topLeftColumn, + public int $topLeftRow, + public int $bottomRightColumn, + public int $bottomRightRow, ) {} } diff --git a/lib/openspout/src/Writer/XLSX/Options.php b/lib/openspout/src/Writer/XLSX/Options.php index 7f27a02b6bf..0cbc1357f92 100644 --- a/lib/openspout/src/Writer/XLSX/Options.php +++ b/lib/openspout/src/Writer/XLSX/Options.php @@ -9,6 +9,7 @@ use OpenSpout\Writer\Common\AbstractOptions; use OpenSpout\Writer\XLSX\Options\HeaderFooter; use OpenSpout\Writer\XLSX\Options\PageMargin; use OpenSpout\Writer\XLSX\Options\PageSetup; +use OpenSpout\Writer\XLSX\Options\WorkbookProtection; final class Options extends AbstractOptions { @@ -26,6 +27,10 @@ final class Options extends AbstractOptions private ?HeaderFooter $headerFooter = null; + private ?WorkbookProtection $workbookProtection = null; + + private Properties $properties; + public function __construct() { parent::__construct(); @@ -35,6 +40,8 @@ final class Options extends AbstractOptions $defaultRowStyle->setFontName(self::DEFAULT_FONT_NAME); $this->DEFAULT_ROW_STYLE = $defaultRowStyle; + + $this->properties = new Properties(); } /** @@ -102,4 +109,24 @@ final class Options extends AbstractOptions { return $this->headerFooter; } + + public function getWorkbookProtection(): ?WorkbookProtection + { + return $this->workbookProtection; + } + + public function setWorkbookProtection(WorkbookProtection $workbookProtection): void + { + $this->workbookProtection = $workbookProtection; + } + + public function getProperties(): Properties + { + return $this->properties; + } + + public function setProperties(Properties $properties): void + { + $this->properties = $properties; + } } diff --git a/lib/openspout/src/Writer/XLSX/Options/HeaderFooter.php b/lib/openspout/src/Writer/XLSX/Options/HeaderFooter.php index cda9dde390b..8a71360a802 100644 --- a/lib/openspout/src/Writer/XLSX/Options/HeaderFooter.php +++ b/lib/openspout/src/Writer/XLSX/Options/HeaderFooter.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace OpenSpout\Writer\XLSX\Options; -final class HeaderFooter +final readonly class HeaderFooter { public function __construct( - public readonly ?string $oddHeader = null, - public readonly ?string $oddFooter = null, - public readonly ?string $evenHeader = null, - public readonly ?string $evenFooter = null, - public readonly bool $differentOddEven = false, + public ?string $oddHeader = null, + public ?string $oddFooter = null, + public ?string $evenHeader = null, + public ?string $evenFooter = null, + public bool $differentOddEven = false, ) {} } diff --git a/lib/openspout/src/Writer/XLSX/Options/PageMargin.php b/lib/openspout/src/Writer/XLSX/Options/PageMargin.php index 4b4ce3b832e..0afaf53b43c 100644 --- a/lib/openspout/src/Writer/XLSX/Options/PageMargin.php +++ b/lib/openspout/src/Writer/XLSX/Options/PageMargin.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace OpenSpout\Writer\XLSX\Options; -final class PageMargin +final readonly class PageMargin { public function __construct( - public readonly float $top = 0.75, - public readonly float $right = 0.7, - public readonly float $bottom = 0.75, - public readonly float $left = 0.7, - public readonly float $header = 0.3, - public readonly float $footer = 0.3 + public float $top = 0.75, + public float $right = 0.7, + public float $bottom = 0.75, + public float $left = 0.7, + public float $header = 0.3, + public float $footer = 0.3 ) {} } diff --git a/lib/openspout/src/Writer/XLSX/Options/PageSetup.php b/lib/openspout/src/Writer/XLSX/Options/PageSetup.php index 8b63622650e..860312cc1b9 100644 --- a/lib/openspout/src/Writer/XLSX/Options/PageSetup.php +++ b/lib/openspout/src/Writer/XLSX/Options/PageSetup.php @@ -4,15 +4,15 @@ declare(strict_types=1); namespace OpenSpout\Writer\XLSX\Options; -final class PageSetup +final readonly class PageSetup { - public readonly bool $fitToPage; + public bool $fitToPage; public function __construct( - public readonly ?PageOrientation $pageOrientation, - public readonly ?PaperSize $paperSize, - public readonly ?int $fitToHeight = null, - public readonly ?int $fitToWidth = null, + public ?PageOrientation $pageOrientation, + public ?PaperSize $paperSize, + public ?int $fitToHeight = null, + public ?int $fitToWidth = null, ) { $this->fitToPage = null !== $fitToHeight || null !== $fitToWidth; } diff --git a/lib/openspout/src/Writer/XLSX/Options/SheetProtection.php b/lib/openspout/src/Writer/XLSX/Options/SheetProtection.php new file mode 100644 index 00000000000..3f8b759b20a --- /dev/null +++ b/lib/openspout/src/Writer/XLSX/Options/SheetProtection.php @@ -0,0 +1,76 @@ +getSheetViewAttributes().'/>'; + } + + private function getSheetViewAttributes(): string + { + return $this->generateAttributes([ + 'password' => null !== $this->password ? PasswordHashHelper::make($this->password) : '', + 'sheet' => $this->lockSheet, + 'objects' => $this->lockObjects, + 'scenarios' => $this->lockScenarios, + 'formatCells' => $this->lockCellFormatting, + 'formatColumns' => $this->lockColumnFormatting, + 'formatRows' => $this->lockRowFormatting, + 'insertColumns' => $this->lockColumnInsert, + 'insertRows' => $this->lockRowInsert, + 'deleteColumns' => $this->lockColumnDelete, + 'deleteRows' => $this->lockRowDelete, + 'selectLockedCells' => $this->lockLockedCellSelection, + 'selectUnlockedCells' => $this->lockUnlockedCellsSelection, + 'autoFilter' => $this->lockAutoFilter, + 'sort' => $this->lockSort, + 'hyperlink' => $this->lockHyperlinkInsert, + 'pivotTables' => $this->lockPivotTables, + ]); + } + + /** + * @param array $data with key containing the attribute name and value containing the attribute value + */ + private function generateAttributes(array $data): string + { + // Create attribute for each key + $attributes = array_map(static function (string $key, bool|string $value): string { + if (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + return $key.'="'.$value.'"'; + }, array_keys($data), $data); + + // Append all attributes + return ' '.implode(' ', $attributes); + } +} diff --git a/lib/openspout/src/Writer/XLSX/Options/WorkbookProtection.php b/lib/openspout/src/Writer/XLSX/Options/WorkbookProtection.php new file mode 100644 index 00000000000..872dc5354a9 --- /dev/null +++ b/lib/openspout/src/Writer/XLSX/Options/WorkbookProtection.php @@ -0,0 +1,50 @@ +getSheetViewAttributes().'/>'; + } + + private function getSheetViewAttributes(): string + { + return $this->generateAttributes([ + 'workbookPassword' => null !== $this->password ? PasswordHashHelper::make($this->password) : '', + 'lockStructure' => $this->lockStructure, + 'lockWindows' => $this->lockWindows, + 'lockRevisions' => $this->lockRevisions, + ]); + } + + /** + * @param array $data with key containing the attribute name and value containing the attribute value + */ + private function generateAttributes(array $data): string + { + // Create attribute for each key + $attributes = array_map(static function (string $key, bool|string $value): string { + if (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + return $key.'="'.$value.'"'; + }, array_keys($data), $data); + + // Append all attributes + return ' '.implode(' ', $attributes); + } +} diff --git a/lib/openspout/src/Writer/XLSX/Properties.php b/lib/openspout/src/Writer/XLSX/Properties.php new file mode 100644 index 00000000000..bbef0092138 --- /dev/null +++ b/lib/openspout/src/Writer/XLSX/Properties.php @@ -0,0 +1,22 @@ + $customProperties */ + public array $customProperties = [], + ) {} +} diff --git a/lib/openspout/src/Writer/XLSX/Writer.php b/lib/openspout/src/Writer/XLSX/Writer.php index cab0676a00e..2615fb2b448 100644 --- a/lib/openspout/src/Writer/XLSX/Writer.php +++ b/lib/openspout/src/Writer/XLSX/Writer.php @@ -35,6 +35,23 @@ final class Writer extends AbstractWriterMultiSheets return $this->options; } + public function setCreator(string $creator): void + { + $props = $this->options->getProperties(); + $this->options->setProperties(new Properties( + $props->title, + $props->subject, + $props->application, + $creator, + $props->lastModifiedBy, + $props->keywords, + $props->description, + $props->category, + $props->language, + $props->customProperties + )); + } + protected function createWorkbookManager(): WorkbookManager { $workbook = new Workbook(); @@ -43,7 +60,7 @@ final class Writer extends AbstractWriterMultiSheets $this->options->getTempFolder(), new ZipHelper(), new XLSX(), - $this->creator + $this->options->getProperties() ); $fileSystemHelper->createBaseFilesAndFolders(); @@ -51,7 +68,12 @@ final class Writer extends AbstractWriterMultiSheets $sharedStringsManager = new SharedStringsManager($xlFolder, new XLSX()); $styleMerger = new StyleMerger(); - $styleManager = new StyleManager(new StyleRegistry($this->options->DEFAULT_ROW_STYLE)); + $escaper = new XLSX(); + + $styleManager = new StyleManager( + new StyleRegistry($this->options->DEFAULT_ROW_STYLE), + $escaper + ); $commentsManager = new CommentsManager($xlFolder, new XLSX()); @@ -61,7 +83,7 @@ final class Writer extends AbstractWriterMultiSheets $styleMerger, $commentsManager, $sharedStringsManager, - new XLSX(), + $escaper, StringHelper::factory() ); diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 0b38cd36752..56b243131a5 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -365,7 +365,7 @@ All rights reserved. openspout OpenSpout Library to read and write spreadsheet files (CSV, XLSX and ODS). - 4.23.0 + 4.28.5 MIT https://github.com/openspout/openspout