From eb450c489ed72234c06a4f1075e7e11be25a7a8e Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:10:42 -0800 Subject: [PATCH 01/24] Ods Writer Horizontal Alignment Fix #4261. Ods does nothing with the `indent` property of Alignment. The fix is easy, but not as easy as it should be. Excel treats indent as an int in some unspecified unit. Ods, on the other hand, specifies it as a float with unit (inches, ems, etc.). Since MS does not reveal what an indent unit is, I resorted to some experimentation. It appears that 1 unit is equal to 0.1043 inches. That is not a guaranteed relationship - the conversion might be non-linear, or it might be affected by external factors like font size. I think multiplying by 0.1043 is adequate for now, and unquestionably better than what we're currently doing (ignoring the property). If it breaks down at some point, we'll look at it again. BTW, Shared\Drawing uses some conversion values of 9525, which, reciprocating and sliding the decimal point along yields a value of 0.10498..., which is close but not close enough for me to use. The other property used in conjunction with indent is `text-align`, and here we have not been doing the right thing. If that property has the default value (`General`), it is treated as `start` (i.e. `left` on LTR docs and presumably `right` on RTL). This will align numbers as if they were text, which does no harm (they're still usable as numbers), but is not how LibreOffice handles them by default. Ods writer is changed to omit `text-align` when it is set to `General`, and let LibreOffice choose the appropriate alignment. These changes are for Writer only. Ods Reader support for styles remains severely lacking. --- src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 20 ++++-- .../Writer/Ods/IndentTest.php | 62 +++++++++++++++++++ tests/data/Writer/Ods/content-arrays.xml | 48 +++++++++++++- tests/data/Writer/Ods/content-empty.xml | 1 - .../Writer/Ods/content-hidden-worksheet.xml | 1 - tests/data/Writer/Ods/content-with-data.xml | 11 ---- 6 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 7573230689..1cc68faba1 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -20,6 +20,7 @@ class Style public const COLUMN_STYLE_PREFIX = 'co'; public const ROW_STYLE_PREFIX = 'ro'; public const TABLE_STYLE_PREFIX = 'ta'; + public const INDENT_TO_INCHES = 0.1043; // undocumented, used trial and error private XMLWriter $writer; @@ -28,12 +29,13 @@ public function __construct(XMLWriter $writer) $this->writer = $writer; } - private function mapHorizontalAlignment(string $horizontalAlignment): string + private function mapHorizontalAlignment(?string $horizontalAlignment): string { return match ($horizontalAlignment) { Alignment::HORIZONTAL_CENTER, Alignment::HORIZONTAL_CENTER_CONTINUOUS, Alignment::HORIZONTAL_DISTRIBUTED => 'center', Alignment::HORIZONTAL_RIGHT => 'end', Alignment::HORIZONTAL_FILL, Alignment::HORIZONTAL_JUSTIFY => 'justify', + Alignment::HORIZONTAL_GENERAL, '', null => '', default => 'start', }; } @@ -145,8 +147,10 @@ private function writeCellProperties(CellStyle $style): void { // Align $hAlign = $style->getAlignment()->getHorizontal(); + $hAlign = $this->mapHorizontalAlignment($hAlign); $vAlign = $style->getAlignment()->getVertical(); $wrap = $style->getAlignment()->getWrapText(); + $indent = $style->getAlignment()->getIndent(); $this->writer->startElement('style:table-cell-properties'); if (!empty($vAlign) || $wrap) { @@ -168,10 +172,16 @@ private function writeCellProperties(CellStyle $style): void $this->writer->endElement(); - if (!empty($hAlign)) { - $hAlign = $this->mapHorizontalAlignment($hAlign); - $this->writer->startElement('style:paragraph-properties'); - $this->writer->writeAttribute('fo:text-align', $hAlign); + if ($hAlign !== '' || !empty($indent)) { + $this->writer + ->startElement('style:paragraph-properties'); + if ($hAlign !== '') { + $this->writer->writeAttribute('fo:text-align', $hAlign); + } + if (!empty($indent)) { + $indentString = sprintf('%.4f', $indent * self::INDENT_TO_INCHES) . 'in'; + $this->writer->writeAttribute('fo:margin-left', $indentString); + } $this->writer->endElement(); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php new file mode 100644 index 0000000000..7b6a62baa2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php @@ -0,0 +1,62 @@ +compatibilityMode = Functions::getCompatibilityMode(); + Functions::setCompatibilityMode( + Functions::COMPATIBILITY_OPENOFFICE + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + Functions::setCompatibilityMode($this->compatibilityMode); + } + + public function testWriteSpreadsheet(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', 'aa'); + $sheet->setCellValue('B1', 'bb'); + $sheet->setCellValue('A2', 'cc'); + $sheet->setCellValue('B2', 'dd'); + $sheet->getStyle('A1')->getAlignment()->setIndent(2); + $writer = new Ods($spreadsheet); + $content = new Content($writer); + $xml = $content->write(); + self::assertStringContainsString( + '' + . '' + . '' + . '', + $xml + ); + self::assertStringContainsString( + '' + . '' + . '' // fo:margin-left is what we're looking for + . '' + . '', + $xml + ); + self::assertSame(3, substr_count($xml, 'table:style-name="ce0"')); + self::assertSame(1, substr_count($xml, 'table:style-name="ce1"')); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml index a33b7dbfca..a363d199d9 100644 --- a/tests/data/Writer/Ods/content-arrays.xml +++ b/tests/data/Writer/Ods/content-arrays.xml @@ -1,2 +1,48 @@ -11133 \ No newline at end of file + + + + + + + + + + + + + + + + + + + +1 + + +1 + + + + + +1 + + +3 + + + + + +3 + + + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 84f4c23977..ee82953d1a 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -8,7 +8,6 @@ - diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml index 88a53257a1..89985861c1 100644 --- a/tests/data/Writer/Ods/content-hidden-worksheet.xml +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -11,7 +11,6 @@ - diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index db7d75a747..5b2f0677e0 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -11,57 +11,46 @@ - - - - - - - - - - - From 8654e805f4e1aebf0aff6feb2df68472dc9bba62 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:14:12 -0800 Subject: [PATCH 02/24] Ods Writer Eliminate Padding at End of Row Ods Writer currently will write something like `` at the end of each row. This is not necessary. In eliminating that, I have made the code a bit more efficient and (hopefully) more readable. --- src/PhpSpreadsheet/Writer/Ods/Content.php | 59 +++++++------------ .../Ods/RepeatEmptyCellsAndRowsTest.php | 48 +++++++++++++++ tests/data/Writer/Ods/content-arrays.xml | 45 +++++++++++++- tests/data/Writer/Ods/content-empty.xml | 4 -- .../Writer/Ods/content-hidden-worksheet.xml | 2 - tests/data/Writer/Ods/content-with-data.xml | 6 -- 6 files changed, 113 insertions(+), 51 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 91ac5d1d21..3b9cc82458 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -20,9 +20,6 @@ */ class Content extends WriterPart { - const NUMBER_COLS_REPEATED_MAX = 1024; - const NUMBER_ROWS_REPEATED_MAX = 1048576; - private Formula $formulaConvertor; /** @@ -142,7 +139,6 @@ private function writeSheets(XMLWriter $objWriter): void sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric()) ); $objWriter->writeAttribute('table:default-cell-style-name', 'ce0'); -// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); $objWriter->endElement(); } $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex); @@ -155,34 +151,33 @@ private function writeSheets(XMLWriter $objWriter): void */ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void { - $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; - $span_row = 0; + $spanRow = 0; $rows = $sheet->getRowIterator(); foreach ($rows as $row) { - $cellIterator = $row->getCellIterator(); - --$numberRowsRepeated; - if ($cellIterator->valid()) { - $objWriter->startElement('table:table-row'); - if ($span_row) { - if ($span_row > 1) { - $objWriter->writeAttribute('table:number-rows-repeated', (string) $span_row); - } - $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX); + $cellIterator = $row->getCellIterator(iterateOnlyExistingCells: true); + $cellIterator->rewind(); + $rowStyleExists = $sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0; + if ($cellIterator->valid() || $rowStyleExists) { + if ($spanRow) { + $objWriter->startElement('table:table-row'); + $objWriter->writeAttribute( + 'table:number-rows-repeated', + (string) $spanRow + ); $objWriter->endElement(); - $span_row = 0; - } else { - if ($sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) { - $objWriter->writeAttribute( - 'table:style-name', - sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) - ); - } - $this->writeCells($objWriter, $cellIterator); + $spanRow = 0; + } + $objWriter->startElement('table:table-row'); + if ($rowStyleExists) { + $objWriter->writeAttribute( + 'table:style-name', + sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) + ); } + $this->writeCells($objWriter, $cellIterator); $objWriter->endElement(); } else { - ++$span_row; + ++$spanRow; } } } @@ -192,7 +187,6 @@ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetInd */ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void { - $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX; $prevColumn = -1; foreach ($cells as $cell) { /** @var Cell $cell */ @@ -293,17 +287,6 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void $objWriter->endElement(); $prevColumn = $column; } - - $numberColsRepeated = $numberColsRepeated - $prevColumn - 1; - if ($numberColsRepeated > 0) { - if ($numberColsRepeated > 1) { - $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', (string) $numberColsRepeated); - $objWriter->endElement(); - } else { - $objWriter->writeElement('table:table-cell'); - } - } } /** diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php new file mode 100644 index 0000000000..b64f8fd1dc --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php @@ -0,0 +1,48 @@ +getActiveSheet(); + $oldSheet->setCellValue('C1', 'xx'); + $oldSheet->setCellValue('G1', 'aa'); + $oldSheet->setCellValue('BB1', 'bb'); + $oldSheet->setCellValue('A6', 'aaa'); + $oldSheet->setCellValue('B7', 'bbb'); + $oldSheet->getRowDimension(10)->setRowHeight(12); + $oldSheet->setCellValue('A12', 'this is A12'); + $style = $oldSheet->getStyle('B14:D14'); + $style->getFont()->setBold(true); + $oldSheet->getCell('E15')->setValue('X'); + $oldSheet->mergeCells('E15:G16'); + $oldSheet->getCell('J15')->setValue('j15'); + $oldSheet->getCell('J16')->setValue('j16'); + $oldSheet->getCell('A19')->setValue('lastrow'); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods'); + $spreadsheetOld->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('xx', $sheet->getCell('C1')->getValue()); + self::assertSame('aa', $sheet->getCell('G1')->getValue()); + self::assertSame('bb', $sheet->getCell('BB1')->getValue()); + self::assertSame('aaa', $sheet->getCell('A6')->getValue()); + self::assertSame('bbb', $sheet->getCell('B7')->getValue()); + self::assertSame('this is A12', $sheet->getCell('A12')->getValue()); + // Read styles, including row height, not yet implemented for ODS + self::assertSame('j15', $sheet->getCell('J15')->getValue()); + self::assertSame('j16', $sheet->getCell('J16')->getValue()); + self::assertSame(['E15:G16' => 'E15:G16'], $sheet->getMergeCells()); + self::assertSame('lastrow', $sheet->getCell('A19')->getValue()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml index a33b7dbfca..939dbea8ca 100644 --- a/tests/data/Writer/Ods/content-arrays.xml +++ b/tests/data/Writer/Ods/content-arrays.xml @@ -1,2 +1,45 @@ -11133 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + +1 + + +1 + + + + +1 + + +3 + + + + +3 + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 84f4c23977..f8efeb542a 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -17,10 +17,6 @@ - - - - diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml index 88a53257a1..8f34a19632 100644 --- a/tests/data/Writer/Ods/content-hidden-worksheet.xml +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -24,7 +24,6 @@ 1 - @@ -33,7 +32,6 @@ 2 - diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index db7d75a747..911566ae04 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -92,7 +92,6 @@ Lorem ipsum - @@ -107,10 +106,6 @@ 42798.572060185 - - - - @@ -119,7 +114,6 @@ 2 - From e2ec705ee8c5bd7d7daeee3d66b8425266287bfc Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:26:40 -0800 Subject: [PATCH 03/24] Ods Writer Master Page Name PR #2850 and PR #2851 added support for Worksheet Visibility to ODS. They work well when LibreOffice is used to open the spreadsheet. However, when Excel tries to open it, it reports corruption. Strictly speaking, this is not a PhpSpreadsheet problem, but, if we can fix it, we should. It took a while to figure out what's bothering Excel. I'm not sure that all of what follows is necessary, but it works. It appears that it wants the content.xml `style:automatic-styles` definition for the `table` (i.e. worksheet) to include a `style:master-page-name` attribute. That attribute requires a corresponding definition in the `office:master-styles` section in styles.xml, and that attribute likewise requires a definition in `office:automatic-styles`. The new entries in styles.xml can be used to specify things like header and footer. However, the ways that these are specified is distinctly different from what Excel (and therefore PhpSpreadsheet) does. Implementing that will be a good future project. However, for now, they will remain unsupported for Ods. --- src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 1 + src/PhpSpreadsheet/Writer/Ods/Styles.php | 13 ++++- tests/data/Writer/Ods/content-arrays.xml | 49 ++++++++++++++++++- tests/data/Writer/Ods/content-empty.xml | 2 +- .../Writer/Ods/content-hidden-worksheet.xml | 4 +- tests/data/Writer/Ods/content-with-data.xml | 4 +- 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 7573230689..c1c9f32c11 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -289,6 +289,7 @@ public function writeTableStyle(Worksheet $worksheet, int $sheetId): void 'style:name', sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId) ); + $this->writer->writeAttribute('style:master-page-name', 'Default'); $this->writer->startElement('style:table-properties'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Styles.php b/src/PhpSpreadsheet/Writer/Ods/Styles.php index 448b1eff13..6bfdfb1b33 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Styles.php +++ b/src/PhpSpreadsheet/Writer/Ods/Styles.php @@ -56,8 +56,17 @@ public function write(): string $objWriter->writeElement('office:font-face-decls'); $objWriter->writeElement('office:styles'); - $objWriter->writeElement('office:automatic-styles'); - $objWriter->writeElement('office:master-styles'); + $objWriter->startElement('office:automatic-styles'); + $objWriter->startElement('style:page-layout'); + $objWriter->writeAttribute('style:name', 'Mpm1'); + $objWriter->endElement(); // style:page-layout + $objWriter->endElement(); // office:automatic-styles + $objWriter->startElement('office:master-styles'); + $objWriter->startElement('style:master-page'); + $objWriter->writeAttribute('style:name', 'Default'); + $objWriter->writeAttribute('style:page-layout-name', 'Mpm1'); + $objWriter->endElement(); //style:master-page + $objWriter->endElement(); //office:master-styles $objWriter->endElement(); return $objWriter->getData(); diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml index a33b7dbfca..231036ee29 100644 --- a/tests/data/Writer/Ods/content-arrays.xml +++ b/tests/data/Writer/Ods/content-arrays.xml @@ -1,2 +1,49 @@ -11133 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + +1 + + +1 + + + + + +1 + + +3 + + + + + +3 + + + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 84f4c23977..cb14ce7e53 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -3,7 +3,7 @@ - + diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml index 88a53257a1..1d89560505 100644 --- a/tests/data/Writer/Ods/content-hidden-worksheet.xml +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -3,10 +3,10 @@ - + - + diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index db7d75a747..82209609f8 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -3,10 +3,10 @@ - + - + From 08c5ff071b73785c80a6780dd4696a7240d340c2 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 11 Dec 2024 04:37:05 -0800 Subject: [PATCH 04/24] Add forceFullCalc Option to Xlsx Writer Fix #4269. In response to issue #456 PR #515, `forceFullCalc` was added to workbook.xml whenever `preCalculateFormulas` was set to false. It is not clear why this should have been needed; attempts to reproduce the error in the original 6.5-year-old issue are unable to reproduce it today. Nevertheless, it is, or was, there for a reason. Today, the forceFullCalc option sets an option where formulas *might* not be recalculated when a cell used in the formula changes. I have not succeeded in finding a situation where it doesn't automatically recalculate, but it probably exists on complicated spreadsheets. To overcome this possibility, Excel offers a button which can be used to recalculate on demand. By itself, this might not be a terrible problem. However, it seems to come with the strange property that any spreadsheets opened at the same time as a forceFullCalc spreadsheet operate as if they too specified forceFullCalc. That *is* a problem, especially since users when closing a spreasheet affected in this way will be prompted to save it even when they haven't changed anything. I am not willing to make a BC change at this time, although I might consider it in future (PR #4240). For now, I am adding a new property `forceFullCalc` with setter (no getter needed) to Xlsx Writer. That property can be `null` (default, in which case the Xml attribute is set as today), or `false` or `true` (in which case the Xml attribute will be set to the Writer attribute). I think that, when `preCalculateFormulas` is set to false, the calling application should give consideration to setting `forceFullCalc` to false as well. All other situations should just use the default. --- docs/topics/reading-and-writing-to-file.md | 6 ++ src/PhpSpreadsheet/Writer/Xlsx.php | 20 +++++- src/PhpSpreadsheet/Writer/Xlsx/Workbook.php | 13 ++-- .../Writer/Xlsx/Issue4269Test.php | 65 +++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 19fe8a8ed9..28bc3b173e 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -169,6 +169,12 @@ $writer->save("05featuredemo.xlsx"); **Note** Formulas will still be calculated in any column set to be autosized even if pre-calculated is set to false +**Note** Prior to release 3.7.0, the use of this feature will cause Excel to be used in a mode where opening a sheet saved in this manner *might* not automatically recalculate a cell's formula when a cell used it the formula changes. Furthermore, that behavior might be applied to all spreadsheets open at the time. To avoid this behavior, add the following statement after `setPreCalculateFormulas` above: +```php +$writer->setForceFullCalc(false); +``` +In a future release, the property's default may change to `false` and that statement may no longer be required. + #### Office 2003 compatibility pack Because of a bug in the Office2003 compatibility pack, there can be some diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index a9d06db581..b74d810502 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -140,6 +140,8 @@ class Xlsx extends BaseWriter private bool $useDynamicArray = false; + private ?bool $forceFullCalc = null; + /** * Create a new Xlsx Writer. */ @@ -342,7 +344,7 @@ public function save($filename, int $flags = 0): void $zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet); // Add workbook to ZIP file - $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas); + $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas, $this->forceFullCalc); $chartCount = 0; // Add worksheets @@ -747,4 +749,20 @@ private function determineUseDynamicArrays(): void { $this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays; } + + /** + * If this is set when a spreadsheet is opened, + * values may not be automatically re-calculated, + * and a button will be available to force re-calculation. + * This may apply to all spreadsheets open at that time. + * If null, this will be set to the opposite of $preCalculateFormulas. + * It is likely that false is the desired setting, although + * cases have been reported where true is required (issue #456). + */ + public function setForceFullCalc(?bool $forceFullCalc): self + { + $this->forceFullCalc = $forceFullCalc; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index 24a2eeb064..c907e100b7 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -15,10 +15,11 @@ class Workbook extends WriterPart * Write workbook to XML format. * * @param bool $preCalculateFormulas If true, formulas will be calculated before writing + * @param ?bool $forceFullCalc If null, !$preCalculateFormulas * * @return string XML Output */ - public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false): string + public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false, ?bool $forceFullCalc = null): string { // Create XML writer if ($this->getParentWriter()->getUseDiskCaching()) { @@ -57,7 +58,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormul (new DefinedNamesWriter($objWriter, $spreadsheet))->write(); // calcPr - $this->writeCalcPr($objWriter, $preCalculateFormulas); + $this->writeCalcPr($objWriter, $preCalculateFormulas, $forceFullCalc); $objWriter->endElement(); @@ -148,7 +149,7 @@ private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spre * * @param bool $preCalculateFormulas If true, formulas will be calculated before writing */ - private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = true): void + private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas, ?bool $forceFullCalc): void { $objWriter->startElement('calcPr'); @@ -160,7 +161,11 @@ private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = // fullCalcOnLoad isn't needed if we will calculate before writing $objWriter->writeAttribute('calcCompleted', ($preCalculateFormulas) ? '1' : '0'); $objWriter->writeAttribute('fullCalcOnLoad', ($preCalculateFormulas) ? '0' : '1'); - $objWriter->writeAttribute('forceFullCalc', ($preCalculateFormulas) ? '0' : '1'); + if ($forceFullCalc === null) { + $objWriter->writeAttribute('forceFullCalc', $preCalculateFormulas ? '0' : '1'); + } else { + $objWriter->writeAttribute('forceFullCalc', $forceFullCalc ? '1' : '0'); + } $objWriter->endElement(); } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php new file mode 100644 index 0000000000..ffe9e30ae0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php @@ -0,0 +1,65 @@ +outputFile !== '') { + unlink($this->outputFile); + $this->outputFile = ''; + } + } + + #[DataProvider('validationProvider')] + public function testWriteArrayFormulaTextJoin( + bool $preCalculateFormulas, + ?bool $forceFullCalc, + string $expected + ): void { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', '=A2*2'); + $sheet->setCellValue('A2', 0); + + $writer = new XlsxWriter($spreadsheet); + $writer->setPreCalculateFormulas($preCalculateFormulas); + if ($forceFullCalc !== null) { + $writer->setForceFullCalc($forceFullCalc); + } + $this->outputFile = File::temporaryFilename(); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFile; + $file .= '#xl/workbook.xml'; + $data = file_get_contents($file); + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString($expected, $data); + } + } + + public static function validationProvider(): array + { + return [ + 'normal case' => [true, null, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="0"'], + 'issue 456' => [false, null, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="1"'], + 'better choice for no precalc' => [false, false, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="0"'], + 'unlikely use case' => [true, true, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="1"'], + ]; + } +} From 3078ea9f8744a3f67f72372589f265b3d008e311 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:10:02 -0800 Subject: [PATCH 05/24] Additional Context Options for https, Restore Disabled Tests Additional Context Options needed, at least sometimes, to read https images. --- src/PhpSpreadsheet/Worksheet/Drawing.php | 12 +++++++++++- .../Calculation/Functions/Logical/XorTest.php | 2 +- .../Reader/Html/HtmlImage2Test.php | 4 ++-- .../PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php | 4 ++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Drawing.php b/src/PhpSpreadsheet/Worksheet/Drawing.php index 35933af3f7..7802714d38 100644 --- a/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -112,7 +112,17 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip $ctx = null; // https://github.com/php/php-src/issues/16023 if (str_starts_with($path, 'https:')) { - $ctx = stream_context_create(['ssl' => ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]]); + $ctx = stream_context_create([ + 'ssl' => ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT], + 'http' => [ + 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'header' => [ + 'Connection: keep-alive', + // accept header used by chrome without image/webp + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + ], + ], + ]); } $imageContents = @file_get_contents($path, false, $ctx); if ($imageContents !== false) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/XorTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/XorTest.php index ea4a8bb066..990068dde1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/XorTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/XorTest.php @@ -18,7 +18,7 @@ public static function providerXOR(): array } #[\PHPUnit\Framework\Attributes\DataProvider('providerXORLiteral')] - public function xtestXORLiteral(mixed $expectedResult, string $formula): void + public function testXORLiteral(mixed $expectedResult, float|string $formula): void { $sheet = $this->getSheet(); $sheet->getCell('A1')->setValue("=XOR($formula)"); diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php index 67e1932371..33dee3108a 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php @@ -10,7 +10,7 @@ class HtmlImage2Test extends TestCase { - public function xtestCanInsertImageGoodProtocol(): void + public function testCanInsertImageGoodProtocol(): void { if (getenv('SKIP_URL_IMAGE_TEST') === '1') { self::markTestSkipped('Skipped due to setting of environment variable'); @@ -31,7 +31,7 @@ public function xtestCanInsertImageGoodProtocol(): void self::assertEquals('A1', $drawing->getCoordinates()); } - public function xtestCantInsertImageNotFound(): void + public function testCantInsertImageNotFound(): void { if (getenv('SKIP_URL_IMAGE_TEST') === '1') { self::markTestSkipped('Skipped due to setting of environment variable'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php index 40d8a571b2..e715621d39 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php @@ -12,7 +12,7 @@ class URLImageTest extends TestCase { - public function xtestURLImageSource(): void + public function testURLImageSource(): void { if (getenv('SKIP_URL_IMAGE_TEST') === '1') { self::markTestSkipped('Skipped due to setting of environment variable'); @@ -37,7 +37,7 @@ public function xtestURLImageSource(): void $spreadsheet->disconnectWorksheets(); } - public function xtestURLImageSourceNotFound(): void + public function testURLImageSourceNotFound(): void { if (getenv('SKIP_URL_IMAGE_TEST') === '1') { self::markTestSkipped('Skipped due to setting of environment variable'); From 62476437abf64f55f4291a1c06676d2120c48a64 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:22:26 -0800 Subject: [PATCH 06/24] Add Context for http Requests --- src/PhpSpreadsheet/Worksheet/Drawing.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Drawing.php b/src/PhpSpreadsheet/Worksheet/Drawing.php index 7802714d38..aeec2b9457 100644 --- a/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -111,9 +111,9 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip $this->isUrl = true; $ctx = null; // https://github.com/php/php-src/issues/16023 - if (str_starts_with($path, 'https:')) { - $ctx = stream_context_create([ - 'ssl' => ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT], + // https://github.com/php/php-src/issues/17121 + if (str_starts_with($path, 'https:') || str_starts_with($path, 'http:')) { + $ctxArray = [ 'http' => [ 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'header' => [ @@ -122,7 +122,11 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', ], ], - ]); + ]; + if (str_starts_with($path, 'https:')) { + $ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]; + } + $ctx = stream_context_create($ctxArray); } $imageContents = @file_get_contents($path, false, $ctx); if ($imageContents !== false) { @@ -193,6 +197,8 @@ public function getIsURL(): bool * Set isURL. * * @return $this + * + * @deprecated 3.7.0 not needed, property is set by setPath */ public function setIsURL(bool $isUrl): self { From cc86557b7718e2da390193b03ee266655415d57c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:18:02 -0800 Subject: [PATCH 07/24] Remove Connection: keep-alive Unacceptable performance. --- CHANGELOG.md | 4 ++-- src/PhpSpreadsheet/Worksheet/Drawing.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59875e9772..f567e658c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,11 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Deprecated -- Nothing yet. +- Drawing::setIsUrl is unneeded. The property is set when setPath determines whether path is a url. ### Fixed -- Nothing yet. +- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) ## 2024-12-08 - 3.6.0 diff --git a/src/PhpSpreadsheet/Worksheet/Drawing.php b/src/PhpSpreadsheet/Worksheet/Drawing.php index aeec2b9457..57c663d91a 100644 --- a/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -117,7 +117,7 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip 'http' => [ 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'header' => [ - 'Connection: keep-alive', + //'Connection: keep-alive', // unacceptable performance // accept header used by chrome without image/webp 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', ], From 92292e58ebe4db52f20bd2567aeb880cde4210d6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:58:27 -0800 Subject: [PATCH 08/24] Simplify Accept Header --- src/PhpSpreadsheet/Worksheet/Drawing.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Drawing.php b/src/PhpSpreadsheet/Worksheet/Drawing.php index 57c663d91a..55450dbfe5 100644 --- a/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -118,8 +118,7 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'header' => [ //'Connection: keep-alive', // unacceptable performance - // accept header used by chrome without image/webp - 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept: image/*;q=0.9,*/*;q=0.8', ], ], ]; From e6d92201fef55eefe224439454855be6a0d18c5a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 14 Dec 2024 09:13:26 -0800 Subject: [PATCH 09/24] Slight Increase in Coverage Reading BIFF8 After breaking up Xls Reader (PR #4118), it is a little easier to identify uncovered code. BIFF8 had no tests involving constant arrays. This PR adds some. Most of the work is in the tests, but some source code is modernized to use things like null coercion. --- src/PhpSpreadsheet/Reader/Xls/Color.php | 8 +--- .../Reader/Xls/ConditionalFormatting.php | 12 +---- .../Reader/Xls/DataValidationHelper.php | 18 ++------ .../Reader/Xls/ListFunctions.php | 8 ++-- .../Reader/Xls/Style/Border.php | 6 +-- .../Reader/Xls/Style/FillPattern.php | 6 +-- .../Reader/Xls/Biff8CoverTest.php | 42 ++++++++++++++++++ tests/data/Reader/XLS/biff8cover.xls | Bin 0 -> 43520 bytes 8 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php create mode 100644 tests/data/Reader/XLS/biff8cover.xls diff --git a/src/PhpSpreadsheet/Reader/Xls/Color.php b/src/PhpSpreadsheet/Reader/Xls/Color.php index 6fd346bfa9..17b6e165ce 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color.php @@ -24,12 +24,6 @@ public static function map(int $color, array $palette, int $version): array return $palette[$color - 8]; } - // default color table - if ($version == Xls::XLS_BIFF8) { - return Color\BIFF8::lookup($color); - } - - // BIFF5 - return Color\BIFF5::lookup($color); + return ($version === Xls::XLS_BIFF8) ? Color\BIFF8::lookup($color) : Color\BIFF5::lookup($color); } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php index 324d12cb21..1c05c63c7e 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php +++ b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php @@ -36,20 +36,12 @@ class ConditionalFormatting extends Xls public static function type(int $type): ?string { - if (isset(self::$types[$type])) { - return self::$types[$type]; - } - - return null; + return self::$types[$type] ?? null; } public static function operator(int $operator): ?string { - if (isset(self::$operators[$operator])) { - return self::$operators[$operator]; - } - - return null; + return self::$operators[$operator] ?? null; } /** diff --git a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php index a0c897efc9..2fef22d211 100644 --- a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php +++ b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php @@ -48,29 +48,17 @@ class DataValidationHelper extends Xls public static function type(int $type): ?string { - if (isset(self::$types[$type])) { - return self::$types[$type]; - } - - return null; + return self::$types[$type] ?? null; } public static function errorStyle(int $errorStyle): ?string { - if (isset(self::$errorStyles[$errorStyle])) { - return self::$errorStyles[$errorStyle]; - } - - return null; + return self::$errorStyles[$errorStyle] ?? null; } public static function operator(int $operator): ?string { - if (isset(self::$operators[$operator])) { - return self::$operators[$operator]; - } - - return null; + return self::$operators[$operator] ?? null; } /** diff --git a/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php b/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php index 2d8778ff25..6477606b06 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php +++ b/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php @@ -44,12 +44,10 @@ protected function listWorksheetNames2(string $filename, Xls $xls): array } foreach ($xls->sheets as $sheet) { - if ($sheet['sheetType'] != 0x00) { + if ($sheet['sheetType'] === 0x00) { // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module - continue; + $worksheetNames[] = $sheet['name']; } - - $worksheetNames[] = $sheet['name']; } return $worksheetNames; @@ -93,7 +91,7 @@ protected function listWorksheetInfo2(string $filename, Xls $xls): array // Parse the individual sheets foreach ($xls->sheets as $sheet) { - if ($sheet['sheetType'] != 0x00) { + if ($sheet['sheetType'] !== 0x00) { // 0x00: Worksheet // 0x02: Chart // 0x06: Visual Basic module diff --git a/src/PhpSpreadsheet/Reader/Xls/Style/Border.php b/src/PhpSpreadsheet/Reader/Xls/Style/Border.php index 97cebbd44e..96275bf9f9 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Style/Border.php +++ b/src/PhpSpreadsheet/Reader/Xls/Style/Border.php @@ -28,10 +28,6 @@ class Border public static function lookup(int $index): string { - if (isset(self::$borderStyleMap[$index])) { - return self::$borderStyleMap[$index]; - } - - return StyleBorder::BORDER_NONE; + return self::$borderStyleMap[$index] ?? StyleBorder::BORDER_NONE; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php b/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php index 4e379509e5..93e3f44979 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php +++ b/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php @@ -37,10 +37,6 @@ class FillPattern */ public static function lookup(int $index): string { - if (isset(self::$fillPatternMap[$index])) { - return self::$fillPatternMap[$index]; - } - - return Fill::FILL_NONE; + return self::$fillPatternMap[$index] ?? Fill::FILL_NONE; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php new file mode 100644 index 0000000000..da9b4f9c98 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php @@ -0,0 +1,42 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=SUM({1;2;3;4;5})', $sheet->getCell('A1')->getValue()); + self::assertSame(15, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame( + '=VLOOKUP("hello",' + . '{"what",1;"why",TRUE;"hello","there";"when",FALSE}' + . ',2,FALSE)', + $sheet->getCell('C1')->getValue() + ); + self::assertSame('there', $sheet->getCell('C1')->getCalculatedValue()); + self::assertSame(2, $sheet->getCell('A3')->getValue()); + self::assertTrue( + $sheet->getStyle('A3')->getFont()->getSuperscript() + ); + self::assertSame('n', $sheet->getCell('B3')->getValue()); + self::assertTrue( + $sheet->getStyle('B3')->getFont()->getSubscript() + ); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLS/biff8cover.xls b/tests/data/Reader/XLS/biff8cover.xls new file mode 100644 index 0000000000000000000000000000000000000000..92da6e12fab2ed3bbd78765c7bac704c484e23c4 GIT binary patch literal 43520 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 literal 0 HcmV?d00001 From d76310d57df727aa723929da0bc51455c5a29ed9 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:18:09 -0800 Subject: [PATCH 10/24] Ignore Coverage for SimpleCache1, ZipStream2 PhpSpreadsheet will use either SimpleCache1/3 (but not both). Likewise for ZipStream2/3. However, only SimpleCache3/Zipstream3 are used while processing code coverage. Indicate this with appropriate annotations. No executable code is changed. --- src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php | 5 +++++ src/PhpSpreadsheet/Writer/ZipStream2.php | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php index b8918c9153..58463ebea0 100644 --- a/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php +++ b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php @@ -9,6 +9,11 @@ * * Alternative implementation should leverage off-memory, non-volatile storage * to reduce overall memory usage. + * + * Either SimpleCache1 or SimpleCache3, but not both, may be used. + * For code coverage testing, it will always be SimpleCache3. + * + * @codeCoverageIgnore */ class SimpleCache1 implements CacheInterface { diff --git a/src/PhpSpreadsheet/Writer/ZipStream2.php b/src/PhpSpreadsheet/Writer/ZipStream2.php index ed21a7e399..e03e1d0cf9 100644 --- a/src/PhpSpreadsheet/Writer/ZipStream2.php +++ b/src/PhpSpreadsheet/Writer/ZipStream2.php @@ -5,6 +5,12 @@ use ZipStream\Option\Archive; use ZipStream\ZipStream; +/** + * Either ZipStream2 or ZipStream3, but not both, may be used. + * For code coverage testing, it will always be ZipStream3. + * + * @codeCoverageIgnore + */ class ZipStream2 { /** From 86a87e093bc2808836e08eb731012f25c2b899f1 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:40:56 -0800 Subject: [PATCH 11/24] Unexpected Charset Possible for Currency Symbol We do not recommend it, but users can call the Php function `setlocale` and that might affect the character used as currency symbol. A problem arises when the caller to setlocale does not specify a character set - in that case, Php will attempt to return its `localeconv()` values in a single-byte character set rather than UTF-8. This is particularly problematic for currency symbols. PhpSpreadsheet till now has accepted such a character, and that can lead to corrupt spreadsheets. It is changed to validate the currency symbol as UTF-8, and fall back to a different choice if not (e.g. EUR rather than 0x80, which is how the euro symbol is depicted in Win-1252). An additional problem arises because Linux systems seem to return the alternate symbol with a trailing blank, but Windows systems do not. To allow callers to get a consistent result, a parameter is added to `getCurrencyCode` which will trim or not (default) the currency code. --- src/PhpSpreadsheet/Shared/StringHelper.php | 59 ++++++++----------- .../Reader/Csv/CsvNumberFormatLocaleTest.php | 10 ++-- .../Shared/StringHelperLocaleTest.php | 56 ++++++++++++++++++ 3 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index aac3836ce4..a16b1cafc0 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -19,17 +19,17 @@ class StringHelper /** * Decimal separator. */ - private static ?string $decimalSeparator; + private static ?string $decimalSeparator = null; /** * Thousands separator. */ - private static ?string $thousandsSeparator; + private static ?string $thousandsSeparator = null; /** * Currency code. */ - private static ?string $currencyCode; + private static ?string $currencyCode = null; /** * Is iconv extension avalable? @@ -511,6 +511,23 @@ public static function strCaseReverse(string $textValue): string return implode('', $characters); } + private static function useAlt(string $altValue, string $default, bool $trimAlt): string + { + return ($trimAlt ? trim($altValue) : $altValue) ?: $default; + } + + private static function getLocaleValue(string $key, string $altKey, string $default, bool $trimAlt = false): string + { + $localeconv = localeconv(); + $rslt = $localeconv[$key]; + // win-1252 implements Euro as 0x80 plus other symbols + if (preg_match('//u', $rslt) !== 1) { + $rslt = ''; + } + + return $rslt ?: self::useAlt($localeconv[$altKey], $default, $trimAlt); + } + /** * Get the decimal separator. If it has not yet been set explicitly, try to obtain number * formatting information from locale. @@ -518,14 +535,7 @@ public static function strCaseReverse(string $textValue): string public static function getDecimalSeparator(): string { if (!isset(self::$decimalSeparator)) { - $localeconv = localeconv(); - self::$decimalSeparator = ($localeconv['decimal_point'] != '') - ? $localeconv['decimal_point'] : $localeconv['mon_decimal_point']; - - if (self::$decimalSeparator == '') { - // Default to . - self::$decimalSeparator = '.'; - } + self::$decimalSeparator = self::getLocaleValue('decimal_point', 'mon_decimal_point', '.'); } return self::$decimalSeparator; @@ -549,14 +559,7 @@ public static function setDecimalSeparator(?string $separator): void public static function getThousandsSeparator(): string { if (!isset(self::$thousandsSeparator)) { - $localeconv = localeconv(); - self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') - ? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep']; - - if (self::$thousandsSeparator == '') { - // Default to . - self::$thousandsSeparator = ','; - } + self::$thousandsSeparator = self::getLocaleValue('thousands_sep', 'mon_thousands_sep', ','); } return self::$thousandsSeparator; @@ -577,22 +580,10 @@ public static function setThousandsSeparator(?string $separator): void * Get the currency code. If it has not yet been set explicitly, try to obtain the * symbol information from locale. */ - public static function getCurrencyCode(): string + public static function getCurrencyCode(bool $trimAlt = false): string { - if (!empty(self::$currencyCode)) { - return self::$currencyCode; - } - self::$currencyCode = '$'; - $localeconv = localeconv(); - if (!empty($localeconv['currency_symbol'])) { - self::$currencyCode = $localeconv['currency_symbol']; - - return self::$currencyCode; - } - if (!empty($localeconv['int_curr_symbol'])) { - self::$currencyCode = $localeconv['int_curr_symbol']; - - return self::$currencyCode; + if (!isset(self::$currencyCode)) { + self::$currencyCode = self::getLocaleValue('currency_symbol', 'int_curr_symbol', '$', $trimAlt); } return self::$currencyCode; diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php index 513576b19b..086d780f53 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php @@ -6,8 +6,12 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Reader\Csv; +use PHPUnit\Framework\Attributes; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +// separate processes due to setLocale +#[Attributes\RunTestsInSeparateProcesses] class CsvNumberFormatLocaleTest extends TestCase { private bool $localeAdjusted; @@ -44,8 +48,7 @@ protected function tearDown(): void } } - #[\PHPUnit\Framework\Attributes\DataProvider('providerNumberFormatNoConversionTest')] - #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + #[DataProvider('providerNumberFormatNoConversionTest')] public function testNumberFormatNoConversion(mixed $expectedValue, string $expectedFormat, string $cellAddress): void { if (!$this->localeAdjusted) { @@ -85,8 +88,7 @@ public static function providerNumberFormatNoConversionTest(): array ]; } - #[\PHPUnit\Framework\Attributes\DataProvider('providerNumberValueConversionTest')] - #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + #[DataProvider('providerNumberValueConversionTest')] public function testNumberValueConversion(mixed $expectedValue, string $cellAddress): void { if (!$this->localeAdjusted) { diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php new file mode 100644 index 0000000000..22d103aa20 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php @@ -0,0 +1,56 @@ +currentLocale = setlocale(LC_ALL, '0'); + } + + protected function tearDown(): void + { + if (is_string($this->currentLocale)) { + setlocale(LC_ALL, $this->currentLocale); + } + StringHelper::setCurrencyCode(null); + } + + public function testCurrency(): void + { + if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu.utf8')) { + self::markTestSkipped('Unable to set German UTF8 locale for testing.'); + } + $result = StringHelper::getCurrencyCode(); + self::assertSame('€', $result); + if (!setlocale(LC_ALL, 'en_us')) { + self::markTestSkipped('Unable to set US locale for testing.'); + } + $result = StringHelper::getCurrencyCode(); + self::assertSame('€', $result, 'result persists despite locale change'); + StringHelper::setCurrencyCode(null); + $result = StringHelper::getCurrencyCode(); + self::assertSame('$', $result, 'locale now used'); + StringHelper::setCurrencyCode(null); + if (!setlocale(LC_ALL, 'deu_deu', 'de_DE')) { + self::markTestSkipped('Unable to set German single-byte locale for testing.'); + } + // Seems like Linux returns trailing blank, Win doesn't + $result = StringHelper::getCurrencyCode(true); // trim if alt symbol is used + self::assertSame('EUR', $result, 'non-UTF8 result ignored'); + } +} From ed158f20ebffa5003c9a0783d47e5228f17bf642 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 15 Dec 2024 10:39:00 -0800 Subject: [PATCH 12/24] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f567e658c6..c06cdf5893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Several fixed to ODS Writer. [Issue #4261](https://github.com/PHPOffice/PhpSpreadsheet/issues/4261) [PR #4263](https://github.com/PHPOffice/PhpSpreadsheet/pull/4263) [PR #4264](https://github.com/PHPOffice/PhpSpreadsheet/pull/4264) [PR #4266](https://github.com/PHPOffice/PhpSpreadsheet/pull/4266) ## 2024-12-08 - 3.6.0 From c744f570c1503e2f0f50e7caac4722e4e7b3ed1a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:48:30 -0800 Subject: [PATCH 13/24] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59875e9772..2cb5435cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing yet. +- Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271) ## 2024-12-08 - 3.6.0 From 06a6549f829653348da7f09eb5349583eba23496 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 22 Dec 2024 20:56:50 -0800 Subject: [PATCH 14/24] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f567e658c6..fedd65221e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Coverage-related tweaks to Xls Reader. [PR #4277](https://github.com/PHPOffice/PhpSpreadsheet/pull/4277) ## 2024-12-08 - 3.6.0 From ea9af88e49f360654e6220a2520259d304cb7a8d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:45:07 -0800 Subject: [PATCH 15/24] Add Language Packs to Ubuntu Images --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8fb4f81978..01cbf16dc9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install locales + run: sudo apt-get install -y language-pack-fr language-pack-de + - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: From f4c576b9d4c896a3148a390c85100d7c23f768cf Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:55:04 -0800 Subject: [PATCH 16/24] Add English Language Pack --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 01cbf16dc9..72148bd081 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Install locales - run: sudo apt-get install -y language-pack-fr language-pack-de + run: sudo apt-get install -y language-pack-fr language-pack-de language-pack-en - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 From 81440e711e74ac3eed9c5085cfdb0845ff39a332 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:12:04 -0800 Subject: [PATCH 17/24] Use en_ca Rather than en_us Installing language-pack-en does not install en_US???? --- tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php index 22d103aa20..b9eaf9b7a8 100644 --- a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php @@ -37,8 +37,8 @@ public function testCurrency(): void } $result = StringHelper::getCurrencyCode(); self::assertSame('€', $result); - if (!setlocale(LC_ALL, 'en_us')) { - self::markTestSkipped('Unable to set US locale for testing.'); + if (!setlocale(LC_ALL, 'en_ca', 'en_CA')) { + self::markTestSkipped('Unable to set en_ca locale for testing.'); } $result = StringHelper::getCurrencyCode(); self::assertSame('€', $result, 'result persists despite locale change'); From eed339fb9c06b2f838197389441cf46bb3ad78e5 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:20:16 -0800 Subject: [PATCH 18/24] Try Default Locale Rather than English --- .github/workflows/main.yml | 2 +- tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 72148bd081..01cbf16dc9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Install locales - run: sudo apt-get install -y language-pack-fr language-pack-de language-pack-en + run: sudo apt-get install -y language-pack-fr language-pack-de - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php index b9eaf9b7a8..505005cb41 100644 --- a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php @@ -37,8 +37,8 @@ public function testCurrency(): void } $result = StringHelper::getCurrencyCode(); self::assertSame('€', $result); - if (!setlocale(LC_ALL, 'en_ca', 'en_CA')) { - self::markTestSkipped('Unable to set en_ca locale for testing.'); + if (!setlocale(LC_ALL, $this->currentLocale)) { + self::markTestSkipped('Unable to restore default locale.'); } $result = StringHelper::getCurrencyCode(); self::assertSame('€', $result, 'result persists despite locale change'); From 12b201b0a4e17c4f83180b9e4691e840383f994d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 23 Dec 2024 02:49:00 -0800 Subject: [PATCH 19/24] Keep Trying --- .github/workflows/main.yml | 3 +++ tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 01cbf16dc9..d640a0b8e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,9 @@ jobs: - name: Install locales run: sudo apt-get install -y language-pack-fr language-pack-de + - name: Install single-byte locale + run: sudo sed -i -e 's/# de_DE@euro/de_DE@euro/g' /etc/locale.gen && sudo locale-gen de_DE@euro + - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php index 505005cb41..c4178f53ab 100644 --- a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php @@ -32,7 +32,7 @@ protected function tearDown(): void public function testCurrency(): void { - if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu.utf8')) { + if ($this->currentLocale === false || !setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu.utf8')) { self::markTestSkipped('Unable to set German UTF8 locale for testing.'); } $result = StringHelper::getCurrencyCode(); @@ -46,10 +46,9 @@ public function testCurrency(): void $result = StringHelper::getCurrencyCode(); self::assertSame('$', $result, 'locale now used'); StringHelper::setCurrencyCode(null); - if (!setlocale(LC_ALL, 'deu_deu', 'de_DE')) { + if (!setlocale(LC_ALL, 'deu_deu', 'de_DE@euro')) { self::markTestSkipped('Unable to set German single-byte locale for testing.'); } - // Seems like Linux returns trailing blank, Win doesn't $result = StringHelper::getCurrencyCode(true); // trim if alt symbol is used self::assertSame('EUR', $result, 'non-UTF8 result ignored'); } From b8f2aa97c2230f2fb9c1006c10d67780ac7c8cb7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 23 Dec 2024 08:53:31 -0800 Subject: [PATCH 20/24] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f567e658c6..f975b47750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Avoid unexpected charset in currency symbol. [PR #4279](https://github.com/PHPOffice/PhpSpreadsheet/pull/4279) ## 2024-12-08 - 3.6.0 From 700a80346be269af668914172bc6f4521982d0b4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 25 Dec 2024 19:11:31 -0800 Subject: [PATCH 21/24] Merge commit from fork * Validate Post Item in Convert-Online.php * Correct Samples - Currency, Accounting, and Downloader * Smarter Color Formatting for Currency * Use helper->log Rather than echo in Convert-Online Responding to comment from @PowerKiki about use of echo for error messages. I do not believe that an exception is warranted, but other scripts use helper->log for error messages, and now so will this one. --- samples/Engineering/Convert-Online.php | 10 +++-- samples/Wizards/NumberFormat/Accounting.php | 19 ++++----- samples/Wizards/NumberFormat/Currency.php | 41 +++++++++++++++---- src/PhpSpreadsheet/Helper/Downloader.php | 6 +-- .../Style/NumberFormat/Formatter.php | 9 ++-- src/PhpSpreadsheet/Writer/Html.php | 11 +++++ 6 files changed, 63 insertions(+), 33 deletions(-) diff --git a/samples/Engineering/Convert-Online.php b/samples/Engineering/Convert-Online.php index a923956b4d..bae13f778c 100644 --- a/samples/Engineering/Convert-Online.php +++ b/samples/Engineering/Convert-Online.php @@ -78,14 +78,16 @@ $quantity = $_POST['quantity']; $fromUnit = $_POST['fromUnit']; $toUnit = $_POST['toUnit']; - if (isset($units[$_POST['category']][$fromUnit], $units[$_POST['category']][$toUnit])) { + if (!is_numeric($quantity)) { + $helper->log('Quantity is not numeric'); + } elseif (isset($units[$_POST['category']][$fromUnit], $units[$_POST['category']][$toUnit])) { /** @var float|string */ $result = ConvertUOM::CONVERT($quantity, $fromUnit, $toUnit); - echo "{$quantity} {$units[$_POST['category']][$fromUnit]} is {$result} {$units[$_POST['category']][$toUnit]}", PHP_EOL; + $helper->log("{$quantity} {$units[$_POST['category']][$fromUnit]} is {$result} {$units[$_POST['category']][$toUnit]}"); } else { - echo 'Please enter quantity and select From Unit and To Unit', PHP_EOL; + $helper->log('Please enter quantity and select From Unit and To Unit'); } } else { - echo 'Please enter quantity and select From Unit and To Unit', PHP_EOL; + $helper->log('Please enter quantity and select From Unit and To Unit'); } diff --git a/samples/Wizards/NumberFormat/Accounting.php b/samples/Wizards/NumberFormat/Accounting.php index 0b87dd4812..e453a5fd31 100644 --- a/samples/Wizards/NumberFormat/Accounting.php +++ b/samples/Wizards/NumberFormat/Accounting.php @@ -64,13 +64,6 @@ >Trailing -
- -
- >Yes - >No -
-

@@ -85,21 +78,23 @@ $helper->log('The Sample Number Value must be numeric'); } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); + } elseif (!in_array($_POST['currency'], array_keys($currencies), true)) { + $helper->log('Unrecognized currency symbol'); } else { try { - $wizard = new Wizard\Accounting($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $wizard = new Wizard\Accounting($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); $helper->log('use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;'); $helper->log( - "\$mask = Wizard\\Accounting('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" + "\$wizard = new Wizard\\Accounting('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" . (isset($_POST['thousands']) ? 'WITH_THOUSANDS_SEPARATOR' : 'WITHOUT_THOUSANDS_SEPARATOR') . ', Wizard\Currency::' . (((bool) $_POST['position']) ? 'LEADING_SYMBOL' : 'TRAILING_SYMBOL') - . ', Wizard\Currency::' . (((bool) $_POST['spacing']) ? 'SYMBOL_WITH_SPACING' : 'SYMBOL_WITHOUT_SPACING') - . ');
' + . ');' ); - $helper->log('echo (string) $mask;'); + $helper->log('$mask = $wizard->format();'); + $helper->log('
echo (string) $mask;'); $helper->log('
Mask:
'); $helper->log($mask . '
'); $helper->log('
Example:
'); diff --git a/samples/Wizards/NumberFormat/Currency.php b/samples/Wizards/NumberFormat/Currency.php index 2c2b248525..c2a66093b2 100644 --- a/samples/Wizards/NumberFormat/Currency.php +++ b/samples/Wizards/NumberFormat/Currency.php @@ -5,6 +5,8 @@ use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard\CurrencyNegative; +use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter; require __DIR__ . '/../Header.php'; @@ -15,6 +17,19 @@ return; } +$negatives = [ + CurrencyNegative::minus, + CurrencyNegative::redMinus, + CurrencyNegative::parentheses, + CurrencyNegative::redParentheses, +]; +$negativesString = [ + 'CurrencyNegative::minus', + 'CurrencyNegative::redMinus', + 'CurrencyNegative::parentheses', + 'CurrencyNegative::redParentheses', +]; + $currencies = [ '$' => 'US Dollars ($)', '€' => 'Euro (€)', @@ -65,10 +80,12 @@
- +
- >Yes - >No + >Minus Sign + >Red Minus Sign + >Parentheses + >Red Parentheses
@@ -85,21 +102,27 @@ $helper->log('The Sample Number Value must be numeric'); } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); + } elseif (!in_array($_POST['currency'], array_keys($currencies), true)) { + $helper->log('Unrecognized currency symbol'); } else { try { - $wizard = new Wizard\Currency($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $negative = $negatives[$_POST['negative']] ?? CurrencyNegative::minus; + $wizard = new Wizard\Currency($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position']); + $wizard->setNegative($negative); $mask = $wizard->format(); - $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); + $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask, [HtmlWriter::class, 'formatColorStatic']); $helper->log('
Code:
'); $helper->log('use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;'); + $helper->log('use PhpOffice\PhpSpreadsheet\Style\NumberFormat\CurrencyNegative;'); $helper->log( - "\$mask = Wizard\\Currency('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" + "\$wizard = new Wizard\\Currency('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" . (isset($_POST['thousands']) ? 'WITH_THOUSANDS_SEPARATOR' : 'WITHOUT_THOUSANDS_SEPARATOR') . ', Wizard\Currency::' . (((bool) $_POST['position']) ? 'LEADING_SYMBOL' : 'TRAILING_SYMBOL') - . ', Wizard\Currency::' . (((bool) $_POST['spacing']) ? 'SYMBOL_WITH_SPACING' : 'SYMBOL_WITHOUT_SPACING') - . ');
' + . ');' ); - $helper->log('echo (string) $mask;'); + $helper->log('$wizard->setNegative(' . $negativesString[$_POST['negative']] . ');'); + $helper->log('$mask = $wizard->format();'); + $helper->log('
echo (string) $mask;'); $helper->log('
Mask:
'); $helper->log($mask . '
'); $helper->log('
Example:
'); diff --git a/src/PhpSpreadsheet/Helper/Downloader.php b/src/PhpSpreadsheet/Helper/Downloader.php index 4e98c7925c..85131dc1df 100644 --- a/src/PhpSpreadsheet/Helper/Downloader.php +++ b/src/PhpSpreadsheet/Helper/Downloader.php @@ -30,18 +30,18 @@ class Downloader public function __construct(string $folder, string $filename, ?string $filetype = null) { if ((is_dir($folder) === false) || (is_readable($folder) === false)) { - throw new Exception("Folder {$folder} is not accessable"); + throw new Exception('Folder is not accessible'); } $filepath = "{$folder}/{$filename}"; $this->filepath = (string) realpath($filepath); $this->filename = basename($filepath); if ((file_exists($this->filepath) === false) || (is_readable($this->filepath) === false)) { - throw new Exception("{$this->filename} not found, or cannot be read"); + throw new Exception('File not found, or cannot be read'); } $filetype ??= pathinfo($filename, PATHINFO_EXTENSION); if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) { - throw new Exception("Invalid filetype: {$filetype} cannot be downloaded"); + throw new Exception('Invalid filetype: file cannot be downloaded'); } $this->filetype = strtolower($filetype); } diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index cb15e8f375..c9032a44d1 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -112,11 +112,11 @@ private static function splitFormatForSectionSelection(array $sections, mixed $v * @param null|array|bool|float|int|RichText|string $value Value to format * @param string $format Format code: see = self::FORMAT_* for predefined values; * or can be any valid MS Excel custom format string - * @param ?array $callBack Callback function for additional formatting of string + * @param null|array|callable $callBack Callback function for additional formatting of string * * @return string Formatted string */ - public static function toFormattedString($value, string $format, ?array $callBack = null): string + public static function toFormattedString($value, string $format, null|array|callable $callBack = null): string { while (is_array($value)) { $value = array_shift($value); @@ -200,9 +200,8 @@ public static function toFormattedString($value, string $format, ?array $callBac } // Additional formatting provided by callback function - if ($callBack !== null) { - [$writerInstance, $function] = $callBack; - $value = $writerInstance->$function($value, $colors); + if (is_callable($callBack)) { + $value = $callBack($value, $colors); } return str_replace(chr(0x00), '.', $value); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index df1353ea08..f73bf53afc 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1720,6 +1720,17 @@ public function setUseInlineCss(bool $useInlineCss): static * @param string $format Format code */ public function formatColor(string $value, string $format): string + { + return self::formatColorStatic($value, $format); + } + + /** + * Add color to formatted string as inline style. + * + * @param string $value Plain formatted value without color + * @param string $format Format code + */ + public static function formatColorStatic(string $value, string $format): string { // Color information, e.g. [Red] is always at the beginning $color = null; // initialize From 45052f88e04c735d56457a8ffcdc40b2635a028e Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 26 Dec 2024 16:34:48 -0800 Subject: [PATCH 22/24] Merge commit from fork --- src/PhpSpreadsheet/Writer/Html.php | 9 ++++--- .../Writer/Html/BadCustomPropertyTest.php | 23 ++++++++++++++++++ .../Writer/Html/BadHyperlinkBaseTest.php | 23 ++++++++++++++++++ .../Writer/Html/BadHyperlinkTest.php | 23 ++++++++++++++++++ tests/data/Reader/XLSX/sec-j47r.dontuse | Bin 0 -> 8884 bytes tests/data/Reader/XLSX/sec-p66w.dontuse | Bin 0 -> 8301 bytes tests/data/Reader/XLSX/sec-q229.dontuse | Bin 0 -> 8940 bytes 7 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php create mode 100644 tests/data/Reader/XLSX/sec-j47r.dontuse create mode 100644 tests/data/Reader/XLSX/sec-p66w.dontuse create mode 100644 tests/data/Reader/XLSX/sec-q229.dontuse diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index f73bf53afc..d70a067f6f 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -403,12 +403,12 @@ public function generateHTMLHeader(bool $includeStyles = false): string } else { $propertyValue = (string) $propertyValue; } - $html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty"); + $html .= self::generateMeta($propertyValue, htmlspecialchars("custom.$propertyQualifier.$customProperty")); } } if (!empty($properties->getHyperlinkBase())) { - $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; } $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); @@ -1586,8 +1586,9 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri // Hyperlink? if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { $url = $worksheet->getHyperlink($coordinate)->getUrl(); - $urldecode = strtolower(html_entity_decode(trim($url), encoding: 'UTF-8')); - $parseScheme = preg_match('/^(\\w+):/', $urldecode, $matches); + $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $urlTrim = preg_replace('/^\\s+/u', '', $urlDecode1) ?? $urlDecode1; + $parseScheme = preg_match('/^([\\w\\s]+):/u', strtolower($urlTrim), $matches); if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) { $cellData = htmlspecialchars($url, Settings::htmlEntityFlags()); } else { diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php new file mode 100644 index 0000000000..a9ef67b791 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php @@ -0,0 +1,23 @@ +load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('', $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php new file mode 100644 index 0000000000..1f12bce570 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php @@ -0,0 +1,23 @@ +load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('', $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php new file mode 100644 index 0000000000..669594bb1c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php @@ -0,0 +1,23 @@ +load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString("jav\tascript:alert()", $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/sec-j47r.dontuse b/tests/data/Reader/XLSX/sec-j47r.dontuse new file mode 100644 index 0000000000000000000000000000000000000000..9914b1ffd12609756e1055090c95380b914dfceb GIT binary patch literal 8884 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLSX/sec-q229.dontuse b/tests/data/Reader/XLSX/sec-q229.dontuse new file mode 100644 index 0000000000000000000000000000000000000000..edb902498776b741084eb98330e431b4d97f7f48 GIT binary patch literal 8940 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 iK#>m>A89Qjl>7Igrig|h004lE_~almB3` Date: Thu, 26 Dec 2024 21:34:47 -0800 Subject: [PATCH 23/24] Prepare Changelog for New Release --- CHANGELOG.md | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c9b49546..b607212073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,19 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## TBD - 3.7.0 - -### Added - -- Nothing yet. - -### Changed - -- Nothing yet. - -### Moved - -- Nothing yet. +## 2024-12-26 - 3.7.0 ### Deprecated @@ -25,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Security patches for Samples. +- Security patches for Html Writer. - Avoid unexpected charset in currency symbol. [PR #4279](https://github.com/PHPOffice/PhpSpreadsheet/pull/4279) - Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271) - More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) From a89b187234b06f8ce7d1d7f48a13849395a346f6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 26 Dec 2024 22:17:03 -0800 Subject: [PATCH 24/24] Prepare Change Log for Next Release --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b607212073..17b5c49bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## TBD - 3.8.0 + +### Added + +- Nothing yet. + +### Changed + +- Nothing yet. + +### Moved + +- Nothing yet. + +### Deprecated + +- Nothing yet. + +### Fixed + +- Nothing yet. + ## 2024-12-26 - 3.7.0 ### Deprecated