PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
← All changes | libraries/vendor/PhpSpreadsheet/Reader/Ods.php +152 -30 3.3.3 → 3.4 View file →
@@ -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 }