| @@ -30,8 +30,9 @@ | ||
| 30 | 30 | use TablePress\PhpOffice\PhpSpreadsheet\Style\Borders; |
| 31 | 31 | use TablePress\PhpOffice\PhpSpreadsheet\Style\Fill; |
| 32 | 32 | use TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
| 33 | 33 | use TablePress\PhpOffice\PhpSpreadsheet\Style\Protection; |
| 34 | +use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Drawing; | |
| 34 | 35 | use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
| 35 | 36 | use Throwable; |
| 36 | 37 | use XMLReader; |
| 37 | 38 | use ZipArchive; |
| @@ -39,8 +40,12 @@ | ||
| 39 | 40 | class Ods extends BaseReader |
| 40 | 41 | { |
| 41 | 42 | const INITIAL_FILE = 'content.xml'; |
| 42 | 43 | |
| 44 | + private ZipArchive $zip; | |
| 45 | + | |
| 46 | + private string $filename; | |
| 47 | + | |
| 43 | 48 | /** |
| 44 | 49 | * Create a new Ods Reader instance. |
| 45 | 50 | */ |
| 46 | 51 | public function __construct() |
| @@ -208,8 +213,9 @@ | ||
| 208 | 213 | $xml->read(); |
| 209 | 214 | if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { |
| 210 | 215 | $rowspan = $xml->getAttribute('table:number-rows-repeated'); |
| 211 | 216 | $rowspan = empty($rowspan) ? 1 : (int) $rowspan; |
| 217 | + self::checkRowsRepeated($currRow, $rowspan); | |
| 212 | 218 | $currRow += $rowspan; |
| 213 | 219 | $currCol = 0; |
| 214 | 220 | // Step into the row |
| 215 | 221 | $xml->read(); |
| @@ -217,8 +223,12 @@ | ||
| 217 | 223 | $doread = true; |
| 218 | 224 | if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { |
| 219 | 225 | $mergeSize = $xml->getAttribute('table:number-columns-repeated'); |
| 220 | 226 | $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize; |
| 227 | + self::checkColumnsRepeatedInt( | |
| 228 | + $currCol, | |
| 229 | + $mergeSize | |
| 230 | + ); | |
| 221 | 231 | $currCol += $mergeSize; |
| 222 | 232 | if (!$xml->isEmptyElement) { |
| 223 | 233 | $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCol); |
| 224 | 234 | $tmpInfo['totalRows'] = $currRow; |
| @@ -226,8 +236,13 @@ | ||
| 226 | 236 | $doread = false; |
| 227 | 237 | } |
| 228 | 238 | } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { |
| 229 | 239 | $mergeSize = $xml->getAttribute('table:number-columns-repeated'); |
| 240 | + $mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize; | |
| 241 | + self::checkColumnsRepeatedInt( | |
| 242 | + $currCol, | |
| 243 | + $mergeSize | |
| 244 | + ); | |
| 230 | 245 | $currCol += (int) $mergeSize; |
| 231 | 246 | } |
| 232 | 247 | if ($doread) { |
| 233 | 248 | $xml->read(); |
| @@ -311,9 +326,10 @@ | ||
| 311 | 326 | public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet |
| 312 | 327 | { |
| 313 | 328 | File::assertFile($filename, self::INITIAL_FILE); |
| 314 | 329 | |
| 315 | - $zip = new ZipArchive(); | |
| 330 | + $this->zip = $zip = new ZipArchive(); | |
| 331 | + $this->filename = $filename; | |
| 316 | 332 | $zip->open($filename); |
| 317 | 333 | |
| 318 | 334 | // Meta |
| 319 | 335 | |
| @@ -321,9 +337,9 @@ | ||
| 321 | 337 | $this->getSecurityScannerOrThrow() |
| 322 | 338 | ->scan($zip->getFromName('meta.xml')) |
| 323 | 339 | ); |
| 324 | 340 | if ($xml === false) { |
| 325 | - throw new Exception('Unable to read data from {$pFilename}'); | |
| 341 | + throw new Exception("Unable to read data from {$filename}"); | |
| 326 | 342 | } |
| 327 | 343 | |
| 328 | 344 | /** @var array{meta?: string, office?: string, dc?: string} */ |
| 329 | 345 | $namespacesMeta = $xml->getNamespaces(true); |
| @@ -332,13 +348,9 @@ | ||
| 332 | 348 | |
| 333 | 349 | // Styles |
| 334 | 350 | |
| 335 | 351 | $this->allStyles = $this->numberFormats = []; |
| 336 | - $dom = new DOMDocument('1.01', 'UTF-8'); | |
| 337 | - $dom->loadXML( | |
| 338 | - $this->getSecurityScannerOrThrow() | |
| 339 | - ->scan($zip->getFromName('styles.xml')) | |
| 340 | - ); | |
| 352 | + $dom = $this->loadDom('styles.xml', $zip); | |
| 341 | 353 | $officeNs = (string) $dom->lookupNamespaceUri('office'); |
| 342 | 354 | $styleNs = (string) $dom->lookupNamespaceUri('style'); |
| 343 | 355 | $fontNs = (string) $dom->lookupNamespaceUri('fo'); |
| 344 | 356 | $numberNs = (string) $dom->lookupNamespaceUri('number'); |
| @@ -402,13 +414,9 @@ | ||
| 402 | 414 | $pageSettings = new PageSettings($dom); |
| 403 | 415 | |
| 404 | 416 | // Main Content |
| 405 | 417 | |
| 406 | - $dom = new DOMDocument('1.01', 'UTF-8'); | |
| 407 | - $dom->loadXML( | |
| 408 | - $this->getSecurityScannerOrThrow() | |
| 409 | - ->scan($zip->getFromName(self::INITIAL_FILE)) | |
| 410 | - ); | |
| 418 | + $dom = $this->loadDom(self::INITIAL_FILE, $zip); | |
| 411 | 419 | |
| 412 | 420 | $pageSettings->readStyleCrossReferences($dom); |
| 413 | 421 | |
| 414 | 422 | $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); |
| @@ -816,8 +824,9 @@ | ||
| 816 | 824 | $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); |
| 817 | 825 | } else { |
| 818 | 826 | $rowRepeats = 1; |
| 819 | 827 | } |
| 828 | + self::checkRowsRepeated($rowID, $rowRepeats); | |
| 820 | 829 | $worksheet = $spreadsheet->getSheetByName($worksheetName); |
| 821 | 830 | |
| 822 | 831 | $columnID = 'A'; |
| 823 | 832 | /** @var DOMElement|DOMText $cellData */ |
| @@ -829,8 +838,10 @@ | ||
| 829 | 838 | $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); |
| 830 | 839 | } else { |
| 831 | 840 | $colRepeats = 1; |
| 832 | 841 | } |
| 842 | + $columnIndex = Coordinate::columnIndexFromString($columnID); | |
| 843 | + self::checkColumnsRepeated($columnID, $colRepeats); | |
| 833 | 844 | $styleName = $cellData->getAttributeNS($tableNs, 'style-name'); |
| 834 | 845 | if ($styleName === '') { |
| 835 | 846 | if ($worksheet === null || !$worksheet->columnDimensionExists($columnID)) { |
| 836 | 847 | $assignedNumberFormat = ''; |
| @@ -850,9 +861,9 @@ | ||
| 850 | 861 | // When a cell has number-columns-repeated, check if ANY column in the |
| 851 | 862 | // repeated range passes the read filter. If not, skip the entire group. |
| 852 | 863 | // If some columns pass, we need to fall through to the processing block |
| 853 | 864 | // which will handle per-column filtering. |
| 854 | - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { | |
| 865 | + if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) { | |
| 855 | 866 | if ($colRepeats <= 1) { |
| 856 | 867 | StringHelper::stringIncrement($columnID); |
| 857 | 868 | |
| 858 | 869 | continue; |
| @@ -864,9 +875,9 @@ | ||
| 864 | 875 | for ($i = 0; $i < $colRepeats; ++$i) { |
| 865 | 876 | if ($i > 0) { |
| 866 | 877 | StringHelper::stringIncrement($tempCol); |
| 867 | 878 | } |
| 868 | - if ($this->getReadFilter()->readCell($tempCol, $rowID, $worksheetName)) { | |
| 879 | + if ($this->readFilter->readCell($tempCol, $rowID, $worksheetName)) { | |
| 869 | 880 | $anyColumnPasses = true; |
| 870 | 881 | |
| 871 | 882 | break; |
| 872 | 883 | } |
| @@ -880,8 +891,10 @@ | ||
| 880 | 891 | continue; |
| 881 | 892 | } |
| 882 | 893 | // Fall through to process the cell, with per-column filter checks |
| 883 | 894 | } |
| 895 | + $tempSpannedRange = "$columnID$rowID"; | |
| 896 | + $spannedRange = ''; | |
| 884 | 897 | if ($worksheet !== null && ($cellData->hasChildNodes() || ($cellData->nextSibling !== null)) && isset($this->allStyles[$styleName])) { |
| 885 | 898 | $spannedRange = "$columnID$rowID"; |
| 886 | 899 | // the following is sufficient for ods, |
| 887 | 900 | // and does no harm for xlsx/xls. |
| @@ -976,8 +989,10 @@ | ||
| 976 | 989 | |
| 977 | 990 | // Filter text:p elements |
| 978 | 991 | if ($item->nodeName == 'text:p') { |
| 979 | 992 | $paragraphs[] = $item; |
| 993 | + } elseif ($item->nodeName === 'draw:frame' && $worksheet !== null) { | |
| 994 | + $this->processDrawFrame($spannedRange ?: $tempSpannedRange, $item, $worksheet); | |
| 980 | 995 | } |
| 981 | 996 | } |
| 982 | 997 | |
| 983 | 998 | if (count($paragraphs) > 0) { |
| @@ -1080,19 +1095,22 @@ | ||
| 1080 | 1095 | case 'date': |
| 1081 | 1096 | $type = DataType::TYPE_NUMERIC; |
| 1082 | 1097 | $value = $cellData->getAttributeNS($officeNs, 'date-value'); |
| 1083 | 1098 | $dataValue = Date::convertIsoDate($value); |
| 1099 | + $format15 = Preg::isMatch('/\d\d\d\d/', $allCellDataText) ? NumberFormat::FORMAT_DATE_XLSX15_YYYY : NumberFormat::FORMAT_DATE_XLSX15; | |
| 1084 | 1100 | |
| 1085 | 1101 | if (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d$/', $allCellDataText)) { |
| 1086 | 1102 | $formatting = 'yyyy-mm-dd'; |
| 1103 | + } elseif (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d \d\d:\d\d(:\d\d)?$/', $allCellDataText)) { | |
| 1104 | + $formatting = NumberFormat::FORMAT_DATE_DATETIME_BETTER; | |
| 1087 | 1105 | } elseif (Preg::isMatch('/^\d\d?-[a-zA-Z]+-\d\d\d\d$/', $allCellDataText)) { |
| 1088 | 1106 | $formatting = 'd-mmm-yyyy'; |
| 1089 | - } elseif ($dataValue != floor($dataValue)) { | |
| 1090 | - $formatting = NumberFormat::FORMAT_DATE_XLSX15 | |
| 1107 | + } elseif ($dataValue != floor($dataValue) || str_contains($allCellDataText, ':')) { | |
| 1108 | + $formatting = $format15 | |
| 1091 | 1109 | . ' ' |
| 1092 | 1110 | . NumberFormat::FORMAT_DATE_TIME4; |
| 1093 | 1111 | } else { |
| 1094 | - $formatting = NumberFormat::FORMAT_DATE_XLSX15; | |
| 1112 | + $formatting = $format15; | |
| 1095 | 1113 | } |
| 1096 | 1114 | |
| 1097 | 1115 | break; |
| 1098 | 1116 | case 'time': |
| @@ -1142,9 +1160,9 @@ | ||
| 1142 | 1160 | if ($i > 0) { |
| 1143 | 1161 | StringHelper::stringIncrement($columnID); |
| 1144 | 1162 | } |
| 1145 | 1163 | |
| 1146 | - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { | |
| 1164 | + if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) { | |
| 1147 | 1165 | continue; |
| 1148 | 1166 | } |
| 1149 | 1167 | |
| 1150 | 1168 | if ($type !== DataType::TYPE_NULL) { |
| @@ -1199,8 +1217,63 @@ | ||
| 1199 | 1217 | } |
| 1200 | 1218 | $rowID += $rowRepeats; |
| 1201 | 1219 | } |
| 1202 | 1220 | |
| 1221 | + private function processDrawFrame(string $spannedRange, DOMElement $item, Worksheet $worksheet): void | |
| 1222 | + { | |
| 1223 | + $drawName = $item->getAttribute('draw:name'); | |
| 1224 | + $svgWidth = $item->getAttribute('svg:width'); | |
| 1225 | + $svgHeight = $item->getAttribute('svg:height'); | |
| 1226 | + $styleName = $item->getAttribute('draw:style-name'); | |
| 1227 | + $drawImage = null; | |
| 1228 | + foreach ($item->childNodes ?? [] as $node) { | |
| 1229 | + // Check if the node is a standard element tag | |
| 1230 | + if ($node->nodeType === XML_ELEMENT_NODE && $node->nodeName === 'draw:image') { | |
| 1231 | + /** @var DOMElement */ | |
| 1232 | + $drawImage = $node; | |
| 1233 | + | |
| 1234 | + break; | |
| 1235 | + } | |
| 1236 | + } | |
| 1237 | + | |
| 1238 | + $xlinkHref = $xlinkType = $xlinkShow = ''; | |
| 1239 | + if ($drawImage !== null) { | |
| 1240 | + $xlinkHref = $drawImage->getAttribute('xlink:href'); | |
| 1241 | + $xlinkType = $drawImage->getAttribute('xlink:type'); | |
| 1242 | + $xlinkShow = $drawImage->getAttribute('xlink:show'); | |
| 1243 | + } | |
| 1244 | + if ( | |
| 1245 | + $drawName !== '' | |
| 1246 | + && Preg::isMatch('/(\d+([.]\d+)?)(cm|in)/', $svgWidth, $matchWidth) | |
| 1247 | + && Preg::isMatch('/(\d+([.]\d+)?)(cm|in)/', $svgHeight, $matchHeight) | |
| 1248 | + //&& $styleName === 'gr1' | |
| 1249 | + && (str_starts_with($xlinkHref, 'Pictures/') || str_starts_with($xlinkHref, 'media/')) | |
| 1250 | + && $xlinkType === 'simple' | |
| 1251 | + && $xlinkShow === 'embed' | |
| 1252 | + ) { | |
| 1253 | + $drawing = new Drawing(); | |
| 1254 | + $drawing->setPath( | |
| 1255 | + "zip://{$this->filename}#$xlinkHref", | |
| 1256 | + true, | |
| 1257 | + $this->zip, | |
| 1258 | + false | |
| 1259 | + ); | |
| 1260 | + $unit = [ | |
| 1261 | + 'cm' => HelperDimension::ABSOLUTE_UNITS[HelperDimension::UOM_CENTIMETERS], | |
| 1262 | + 'in' => HelperDimension::ABSOLUTE_UNITS[HelperDimension::UOM_INCHES], | |
| 1263 | + ]; | |
| 1264 | + $width = ((float) $matchWidth[1]) * $unit[$matchWidth[3]]; | |
| 1265 | + $height = ((float) $matchHeight[1]) * $unit[$matchHeight[3]]; | |
| 1266 | + if ($drawing->getPath()) { | |
| 1267 | + $drawing->setCoordinates($spannedRange) | |
| 1268 | + ->setWidth((int) $width) | |
| 1269 | + ->setHeight((int) $height) | |
| 1270 | + ->setName($drawName) | |
| 1271 | + ->setWorksheet($worksheet); | |
| 1272 | + } | |
| 1273 | + } | |
| 1274 | + } | |
| 1275 | + | |
| 1203 | 1276 | private static function extractNodeName(string $key): string |
| 1204 | 1277 | { |
| 1205 | 1278 | // Remove ns from node name |
| 1206 | 1279 | if (str_contains($key, ':')) { |
| @@ -1312,12 +1385,14 @@ | ||
| 1312 | 1385 | bool $processWidths = true, |
| 1313 | 1386 | bool $processStyles = true |
| 1314 | 1387 | ): void { |
| 1315 | 1388 | if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { |
| 1316 | - $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); | |
| 1389 | + $colRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); | |
| 1317 | 1390 | } else { |
| 1318 | - $rowRepeats = 1; | |
| 1391 | + $colRepeats = 1; | |
| 1319 | 1392 | } |
| 1393 | + // called routine expects index to be 1 less than it is | |
| 1394 | + self::checkColumnsRepeatedInt($tableColumnIndex - 1, $colRepeats); | |
| 1320 | 1395 | $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); |
| 1321 | 1396 | if ($processWidths) { |
| 1322 | 1397 | if (isset($columnWidths[$tableStyleName])) { |
| 1323 | 1398 | $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); |
| @@ -1322,9 +1397,9 @@ | ||
| 1322 | 1397 | if (isset($columnWidths[$tableStyleName])) { |
| 1323 | 1398 | $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); |
| 1324 | 1399 | $tableColumnIndex2 = $tableColumnIndex; |
| 1325 | 1400 | $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2); |
| 1326 | - for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) { | |
| 1401 | + for ($colRepeats2 = $colRepeats; $colRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$colRepeats2) { | |
| 1327 | 1402 | if (!$this->readEmptyCells && $tableColumnIndex2 > $this->highestDataIndex) { |
| 1328 | 1403 | break; |
| 1329 | 1404 | } |
| 1330 | 1405 | $spreadsheet->getActiveSheet() |
| @@ -1341,9 +1416,9 @@ | ||
| 1341 | 1416 | $defaultStyleName = $childNode->getAttributeNS($tableNs, 'default-cell-style-name'); |
| 1342 | 1417 | if ($defaultStyleName !== 'Default' && isset($this->allStyles[$defaultStyleName])) { |
| 1343 | 1418 | $tableColumnIndex2 = $tableColumnIndex; |
| 1344 | 1419 | $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex2); |
| 1345 | - for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) { | |
| 1420 | + for ($colRepeats2 = $colRepeats; $colRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$colRepeats2) { | |
| 1346 | 1421 | $spreadsheet->getActiveSheet() |
| 1347 | 1422 | ->getStyle($tableColumnString) |
| 1348 | 1423 | ->applyFromArray( |
| 1349 | 1424 | $this->allStyles[$defaultStyleName] |
| @@ -1354,18 +1429,14 @@ | ||
| 1354 | 1429 | ++$tableColumnIndex2; |
| 1355 | 1430 | } |
| 1356 | 1431 | } |
| 1357 | 1432 | } |
| 1358 | - $tableColumnIndex += $rowRepeats; | |
| 1433 | + $tableColumnIndex += $colRepeats; | |
| 1359 | 1434 | } |
| 1360 | 1435 | |
| 1361 | 1436 | private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void |
| 1362 | 1437 | { |
| 1363 | - $dom = new DOMDocument('1.01', 'UTF-8'); | |
| 1364 | - $dom->loadXML( | |
| 1365 | - $this->getSecurityScannerOrThrow() | |
| 1366 | - ->scan($zip->getFromName('settings.xml')) | |
| 1367 | - ); | |
| 1438 | + $dom = $this->loadDom('settings.xml', $zip); | |
| 1368 | 1439 | $configNs = (string) $dom->lookupNamespaceUri('config'); |
| 1369 | 1440 | $officeNs = (string) $dom->lookupNamespaceUri('office'); |
| 1370 | 1441 | $settings = $dom->getElementsByTagNameNS($officeNs, 'settings') |
| 1371 | 1442 | ->item(0); |
| @@ -1750,9 +1821,9 @@ | ||
| 1750 | 1821 | $borders = []; |
| 1751 | 1822 | $temp = $tableCellProperties->getAttributeNs($fontNs, 'border'); |
| 1752 | 1823 | $diagonalIndex = Borders::DIAGONAL_NONE; |
| 1753 | 1824 | foreach (['bottom', 'left', 'right', 'top', 'diagonal-tl-br', 'diagonal-bl-tr'] as $direction) { |
| 1754 | - if (str_starts_with($direction, 'diagonal')) { | |
| 1825 | + if ($direction === 'diagonal-tl-br' || $direction === 'diagonal-bl-tr') { | |
| 1755 | 1826 | $directionIndex = 'diagonal'; |
| 1756 | 1827 | $temp = $tableCellProperties->getAttributeNs($styleNs, $direction); |
| 1757 | 1828 | } else { |
| 1758 | 1829 | $directionIndex = $direction; |
| @@ -1778,9 +1849,9 @@ | ||
| 1778 | 1849 | if ($diagonalIndex !== Borders::DIAGONAL_NONE) { |
| 1779 | 1850 | $borders['diagonalDirection'] = $diagonalIndex; |
| 1780 | 1851 | } |
| 1781 | 1852 | |
| 1782 | - return $borders; // @phpstan-ignore-line | |
| 1853 | + return $borders; | |
| 1783 | 1854 | } |
| 1784 | 1855 | |
| 1785 | 1856 | protected function processSomeNumberFormats(?DOMElement $automaticStyle0, string $numberNs, string $styleNs): void |
| 1786 | 1857 | { |
| @@ -1799,6 +1870,57 @@ | ||
| 1799 | 1870 | if ($decimalPlaces === '0' && $minIntegerDigits > 1) { |
| 1800 | 1871 | $this->numberFormats[$styleName] = str_repeat('0', $minIntegerDigits); |
| 1801 | 1872 | } |
| 1802 | 1873 | } |
| 1874 | + } | |
| 1875 | + | |
| 1876 | + private static function checkRowsRepeated(int $rowID, int $rowRepeats): void | |
| 1877 | + { | |
| 1878 | + if ($rowRepeats < 1 || $rowID + $rowRepeats - 1 > AddressRange::MAX_ROW) { | |
| 1879 | + throw new Exception("Invalid number-rows-repeated $rowRepeats following row $rowID"); | |
| 1880 | + } | |
| 1881 | + } | |
| 1882 | + | |
| 1883 | + private static function checkColumnsRepeated(string $colID, int $colRepeats): void | |
| 1884 | + { | |
| 1885 | + $colIndex = Coordinate::columnIndexFromString($colID); | |
| 1886 | + if ($colRepeats < 1 || $colIndex + $colRepeats - 1 > AddressRange::MAX_COLUMN_INT) { | |
| 1887 | + throw new Exception("Invalid number-columns-repeated $colRepeats following column $colID"); | |
| 1888 | + } | |
| 1889 | + } | |
| 1890 | + | |
| 1891 | + private static function checkColumnsRepeatedInt(int $colIndex, int $colRepeats): void | |
| 1892 | + { | |
| 1893 | + // We don't have column string at this point, | |
| 1894 | + // and colIndex is actually 1 less than it should be. | |
| 1895 | + if ($colRepeats < 1 || $colIndex + $colRepeats > AddressRange::MAX_COLUMN_INT) { | |
| 1896 | + throw new Exception("Invalid number-columns-repeated $colRepeats following column index $colIndex"); | |
| 1897 | + } | |
| 1898 | + } | |
| 1899 | + | |
| 1900 | + private function loadDom(string $file, ZipArchive $zip): DOMDocument | |
| 1901 | + { | |
| 1902 | + $dom = new DOMDocument('1.01', 'UTF-8'); | |
| 1903 | + $orig = false; | |
| 1904 | + | |
| 1905 | + try { | |
| 1906 | + $orig = libxml_use_internal_errors(true); | |
| 1907 | + $result = $dom->loadXML( | |
| 1908 | + $this->getSecurityScannerOrThrow() | |
| 1909 | + ->scan($zip->getFromName($file)) | |
| 1910 | + ); | |
| 1911 | + if ($result === false) { | |
| 1912 | + $fatal = false; | |
| 1913 | + foreach (libxml_get_errors() as $err) { | |
| 1914 | + if ($err->level === LIBXML_ERR_FATAL) { | |
| 1915 | + throw new Exception($err->message); | |
| 1916 | + } | |
| 1917 | + } | |
| 1918 | + } | |
| 1919 | + } finally { | |
| 1920 | + libxml_clear_errors(); | |
| 1921 | + libxml_use_internal_errors($orig); | |
| 1922 | + } | |
| 1923 | + | |
| 1924 | + return $dom; | |
| 1803 | 1925 | } |
| 1804 | 1926 | } |