From 2de29384aa11223ee08136c0e6a9e9981bc1a2eb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:36:53 -0700 Subject: [PATCH] Merge commit from fork --- src/PhpSpreadsheet/Reader/Ods.php | 51 +++++++++++++++--- .../Reader/Ods/BadRepeatsTest.php | 48 +++++++++++++++++ tests/data/Reader/Ods/BadRepeatCol.ods | Bin 0 -> 8383 bytes tests/data/Reader/Ods/BadRepeatRow.ods | Bin 0 -> 8334 bytes 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/BadRepeatsTest.php create mode 100644 tests/data/Reader/Ods/BadRepeatCol.ods create mode 100644 tests/data/Reader/Ods/BadRepeatRow.ods diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 08fb4f19a..bb4b7b1ef 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -214,6 +214,7 @@ class Ods extends BaseReader if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { $rowspan = $xml->getAttribute('table:number-rows-repeated'); $rowspan = empty($rowspan) ? 1 : (int) $rowspan; + self::checkRowsRepeated($currRow, $rowspan); $currRow += $rowspan; $currCol = 0; // Step into the row @@ -223,6 +224,10 @@ class Ods extends BaseReader if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { $mergeSize = $xml->getAttribute('table:number-columns-repeated'); $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize; + self::checkColumnsRepeatedInt( + $currCol, + $mergeSize + ); $currCol += $mergeSize; if (!$xml->isEmptyElement) { $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCol); @@ -232,6 +237,11 @@ class Ods extends BaseReader } } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { $mergeSize = $xml->getAttribute('table:number-columns-repeated'); + $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize; + self::checkColumnsRepeatedInt( + $currCol, + $mergeSize + ); $currCol += (int) $mergeSize; } if ($doread) { @@ -328,7 +338,7 @@ class Ods extends BaseReader ->scan($zip->getFromName('meta.xml')) ); if ($xml === false) { - throw new Exception('Unable to read data from {$pFilename}'); + throw new Exception("Unable to read data from {$filename}"); } /** @var array{meta?: string, office?: string, dc?: string} */ @@ -815,6 +825,7 @@ class Ods extends BaseReader } else { $rowRepeats = 1; } + self::checkRowsRepeated($rowID, $rowRepeats); $worksheet = $spreadsheet->getSheetByName($worksheetName); $columnID = 'A'; @@ -828,6 +839,8 @@ class Ods extends BaseReader } else { $colRepeats = 1; } + $columnIndex = Coordinate::columnIndexFromString($columnID); + self::checkColumnsRepeated($columnID, $colRepeats); $styleName = $cellData->getAttributeNS($tableNs, 'style-name'); if ($styleName === '') { if ($worksheet === null || !$worksheet->columnDimensionExists($columnID)) { @@ -1373,17 +1386,19 @@ class Ods extends BaseReader bool $processStyles = true ): void { if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { - $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); + $colRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); } else { - $rowRepeats = 1; + $colRepeats = 1; } + // called routine expects index to be 1 less than it is + self::checkColumnsRepeatedInt($tableColumnIndex - 1, $colRepeats); $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); if ($processWidths) { if (isset($columnWidths[$tableStyleName])) { $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); $tableColumnIndex2 = $tableColumnIndex; $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2); - for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) { + for ($colRepeats2 = $colRepeats; $colRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$colRepeats2) { if (!$this->readEmptyCells && $tableColumnIndex2 > $this->highestDataIndex) { break; } @@ -1402,7 +1417,7 @@ class Ods extends BaseReader if ($defaultStyleName !== 'Default' && isset($this->allStyles[$defaultStyleName])) { $tableColumnIndex2 = $tableColumnIndex; $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2); - for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) { + for ($colRepeats2 = $colRepeats; $colRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$colRepeats2) { $spreadsheet->getActiveSheet() ->getStyle($tableColumnString) ->applyFromArray( @@ -1415,7 +1430,7 @@ class Ods extends BaseReader } } } - $tableColumnIndex += $rowRepeats; + $tableColumnIndex += $colRepeats; } private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void @@ -1858,6 +1873,30 @@ class Ods extends BaseReader } } + private static function checkRowsRepeated(int $rowID, int $rowRepeats): void + { + if ($rowRepeats < 1 || $rowID + $rowRepeats - 1 > AddressRange::MAX_ROW) { + throw new Exception("Invalid number-rows-repeated $rowRepeats following row $rowID"); + } + } + + private static function checkColumnsRepeated(string $colID, int $colRepeats): void + { + $colIndex = Coordinate::columnIndexFromString($colID); + if ($colRepeats < 1 || $colIndex + $colRepeats - 1 > AddressRange::MAX_COLUMN_INT) { + throw new Exception("Invalid number-columns-repeated $colRepeats following column $colID"); + } + } + + private static function checkColumnsRepeatedInt(int $colIndex, int $colRepeats): void + { + // We don't have column string at this point, + // and colIndex is actually 1 less than it should be. + if ($colRepeats < 1 || $colIndex + $colRepeats > AddressRange::MAX_COLUMN_INT) { + throw new Exception("Invalid number-columns-repeated $colRepeats following column index $colIndex"); + } + } + private function loadDom(string $file, ZipArchive $zip): DOMDocument { $dom = new DOMDocument('1.01', 'UTF-8'); diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/BadRepeatsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/BadRepeatsTest.php new file mode 100644 index 000000000..2a8cb7758 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/BadRepeatsTest.php @@ -0,0 +1,48 @@ +expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid number-columns-repeated'); + $reader = new Ods(); + $infile = 'tests/data/Reader/Ods/BadRepeatCol.ods'; + $reader->load($infile); + } + + public function testBadRepeatedColsWorksheetInfo(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid number-columns-repeated'); + $reader = new Ods(); + $infile = 'tests/data/Reader/Ods/BadRepeatCol.ods'; + $reader->listWorksheetInfo($infile); + } + + public function testBadRepeatedRowsRead(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid number-rows-repeated'); + $reader = new Ods(); + $infile = 'tests/data/Reader/Ods/BadRepeatRow.ods'; + $reader->load($infile); + } + + public function testBadRepeatedRowsWorksheetInfo(): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid number-rows-repeated'); + $reader = new Ods(); + $infile = 'tests/data/Reader/Ods/BadRepeatRow.ods'; + $reader->listWorksheetInfo($infile); + } +} diff --git a/tests/data/Reader/Ods/BadRepeatCol.ods b/tests/data/Reader/Ods/BadRepeatCol.ods new file mode 100644 index 0000000000000000000000000000000000000000..2cc40375693cedb7e4d17d720ac6422806cbf970 GIT binary patch literal 8383 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 Wl;jZ*AD5xPegaQmxj*USzyAj=j%nKf literal 0 HcmV?d00001