PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Reader / Ods.php

Ods.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php

822 lines 35.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PhpOffice\PhpSpreadsheet\Reader;
4
5 use DOMAttr;
6 use DOMDocument;
7 use DOMElement;
8 use DOMNode;
9 use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10 use PhpOffice\PhpSpreadsheet\Cell\DataType;
11 use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
12 use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
13 use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
14 use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
15 use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings;
16 use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
17 use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
18 use PhpOffice\PhpSpreadsheet\RichText\RichText;
19 use PhpOffice\PhpSpreadsheet\Shared\Date;
20 use PhpOffice\PhpSpreadsheet\Shared\File;
21 use PhpOffice\PhpSpreadsheet\Spreadsheet;
22 use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
23 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
24 use Throwable;
25 use XMLReader;
26 use ZipArchive;
27
28 class Ods extends BaseReader
29 {
30 const INITIAL_FILE = 'content.xml';
31
32 /**
33 * Create a new Ods Reader instance.
34 */
35 public function __construct()
36 {
37 parent::__construct();
38 $this->securityScanner = XmlScanner::getInstance($this);
39 }
40
41 /**
42 * Can the current IReader read the file?
43 */
44 public function canRead(string $filename): bool
45 {
46 $mimeType = 'UNKNOWN';
47
48 // Load file
49
50 if (File::testFileNoThrow($filename, '')) {
51 $zip = new ZipArchive();
52 if ($zip->open($filename) === true) {
53 // check if it is an OOXML archive
54 $stat = $zip->statName('mimetype');
55 if (!empty($stat) && ($stat['size'] <= 255)) {
56 $mimeType = $zip->getFromName($stat['name']);
57 } elseif ($zip->statName('META-INF/manifest.xml')) {
58 $xml = simplexml_load_string(
59 $this->getSecurityScannerOrThrow()
60 ->scan(
61 $zip->getFromName(
62 'META-INF/manifest.xml'
63 )
64 )
65 );
66 if ($xml !== false) {
67 $namespacesContent = $xml->getNamespaces(true);
68 if (isset($namespacesContent['manifest'])) {
69 $manifest = $xml->children($namespacesContent['manifest']);
70 foreach ($manifest as $manifestDataSet) {
71 /** @scrutinizer ignore-call */
72 $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']);
73 if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') {
74 $mimeType = (string) $manifestAttributes->{'media-type'};
75
76 break;
77 }
78 }
79 }
80 }
81 }
82
83 $zip->close();
84 }
85 }
86
87 return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
88 }
89
90 /**
91 * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
92 *
93 * @param string $filename
94 *
95 * @return string[]
96 */
97 public function listWorksheetNames($filename)
98 {
99 File::assertFile($filename, self::INITIAL_FILE);
100
101 $worksheetNames = [];
102
103 $xml = new XMLReader();
104 $xml->xml(
105 $this->getSecurityScannerOrThrow()
106 ->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE)
107 );
108 $xml->setParserProperty(2, true);
109
110 // Step into the first level of content of the XML
111 $xml->read();
112 while ($xml->read()) {
113 // Quickly jump through to the office:body node
114 while (self::getXmlName($xml) !== 'office:body') {
115 if ($xml->isEmptyElement) {
116 $xml->read();
117 } else {
118 $xml->next();
119 }
120 }
121 // Now read each node until we find our first table:table node
122 while ($xml->read()) {
123 $xmlName = self::getXmlName($xml);
124 if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
125 // Loop through each table:table node reading the table:name attribute for each worksheet name
126 do {
127 $worksheetName = $xml->getAttribute('table:name');
128 if (!empty($worksheetName)) {
129 $worksheetNames[] = $worksheetName;
130 }
131 $xml->next();
132 } while (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
133 }
134 }
135 }
136
137 return $worksheetNames;
138 }
139
140 /**
141 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
142 *
143 * @param string $filename
144 *
145 * @return array
146 */
147 public function listWorksheetInfo($filename)
148 {
149 File::assertFile($filename, self::INITIAL_FILE);
150
151 $worksheetInfo = [];
152
153 $xml = new XMLReader();
154 $xml->xml(
155 $this->getSecurityScannerOrThrow()
156 ->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE)
157 );
158 $xml->setParserProperty(2, true);
159
160 // Step into the first level of content of the XML
161 $xml->read();
162 while ($xml->read()) {
163 // Quickly jump through to the office:body node
164 while (self::getXmlName($xml) !== 'office:body') {
165 if ($xml->isEmptyElement) {
166 $xml->read();
167 } else {
168 $xml->next();
169 }
170 }
171 // Now read each node until we find our first table:table node
172 while ($xml->read()) {
173 if (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
174 $worksheetNames[] = $xml->getAttribute('table:name');
175
176 $tmpInfo = [
177 'worksheetName' => $xml->getAttribute('table:name'),
178 'lastColumnLetter' => 'A',
179 'lastColumnIndex' => 0,
180 'totalRows' => 0,
181 'totalColumns' => 0,
182 ];
183
184 // Loop through each child node of the table:table element reading
185 $currCells = 0;
186 do {
187 $xml->read();
188 if (self::getXmlName($xml) == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
189 $rowspan = $xml->getAttribute('table:number-rows-repeated');
190 $rowspan = empty($rowspan) ? 1 : $rowspan;
191 $tmpInfo['totalRows'] += $rowspan;
192 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
193 $currCells = 0;
194 // Step into the row
195 $xml->read();
196 do {
197 $doread = true;
198 if (self::getXmlName($xml) == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
199 if (!$xml->isEmptyElement) {
200 ++$currCells;
201 $xml->next();
202 $doread = false;
203 }
204 } elseif (self::getXmlName($xml) == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
205 $mergeSize = $xml->getAttribute('table:number-columns-repeated');
206 $currCells += (int) $mergeSize;
207 }
208 if ($doread) {
209 $xml->read();
210 }
211 } while (self::getXmlName($xml) != 'table:table-row');
212 }
213 } while (self::getXmlName($xml) != 'table:table');
214
215 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
216 $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
217 $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
218 $worksheetInfo[] = $tmpInfo;
219 }
220 }
221 }
222
223 return $worksheetInfo;
224 }
225
226 /**
227 * Counteract Phpstan caching.
228 *
229 * @phpstan-impure
230 */
231 private static function getXmlName(XMLReader $xml): string
232 {
233 return $xml->name;
234 }
235
236 /**
237 * Loads PhpSpreadsheet from file.
238 */
239 protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
240 {
241 // Create new Spreadsheet
242 $spreadsheet = new Spreadsheet();
243
244 // Load into this instance
245 return $this->loadIntoExisting($filename, $spreadsheet);
246 }
247
248 /**
249 * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
250 *
251 * @param string $filename
252 *
253 * @return Spreadsheet
254 */
255 public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
256 {
257 File::assertFile($filename, self::INITIAL_FILE);
258
259 $zip = new ZipArchive();
260 $zip->open($filename);
261
262 // Meta
263
264 $xml = @simplexml_load_string(
265 $this->getSecurityScannerOrThrow()
266 ->scan($zip->getFromName('meta.xml'))
267 );
268 if ($xml === false) {
269 throw new Exception('Unable to read data from {$pFilename}');
270 }
271
272 $namespacesMeta = $xml->getNamespaces(true);
273
274 (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
275
276 // Styles
277
278 $dom = new DOMDocument('1.01', 'UTF-8');
279 $dom->loadXML(
280 $this->getSecurityScannerOrThrow()
281 ->scan($zip->getFromName('styles.xml'))
282 );
283
284 $pageSettings = new PageSettings($dom);
285
286 // Main Content
287
288 $dom = new DOMDocument('1.01', 'UTF-8');
289 $dom->loadXML(
290 $this->getSecurityScannerOrThrow()
291 ->scan($zip->getFromName(self::INITIAL_FILE))
292 );
293
294 $officeNs = $dom->lookupNamespaceUri('office');
295 $tableNs = $dom->lookupNamespaceUri('table');
296 $textNs = $dom->lookupNamespaceUri('text');
297 $xlinkNs = $dom->lookupNamespaceUri('xlink');
298 $styleNs = $dom->lookupNamespaceUri('style');
299
300 $pageSettings->readStyleCrossReferences($dom);
301
302 $autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
303 $definedNameReader = new DefinedNames($spreadsheet, $tableNs);
304 $columnWidths = [];
305 $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
306 $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
307 foreach ($automaticStyles as $automaticStyle) {
308 $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
309 $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
310 if ($styleFamily === 'table-column') {
311 $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
312 if ($tcprops !== null) {
313 $tcprop = $tcprops->item(0);
314 if ($tcprop !== null) {
315 $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
316 $columnWidths[$styleName] = $columnWidth;
317 }
318 }
319 }
320 }
321
322 // Content
323 $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
324 $spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet');
325
326 foreach ($spreadsheets as $workbookData) {
327 /** @var DOMElement $workbookData */
328 $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
329
330 $worksheetID = 0;
331 foreach ($tables as $worksheetDataSet) {
332 /** @var DOMElement $worksheetDataSet */
333 $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
334
335 // Check loadSheetsOnly
336 if (
337 $this->loadSheetsOnly !== null
338 && $worksheetName
339 && !in_array($worksheetName, $this->loadSheetsOnly)
340 ) {
341 continue;
342 }
343
344 $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name');
345
346 // Create sheet
347 if ($worksheetID > 0) {
348 $spreadsheet->createSheet(); // First sheet is added by default
349 }
350 $spreadsheet->setActiveSheetIndex($worksheetID);
351
352 if ($worksheetName || is_numeric($worksheetName)) {
353 // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
354 // formula cells... during the load, all formulae should be correct, and we're simply
355 // bringing the worksheet name in line with the formula, not the reverse
356 $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false);
357 }
358
359 // Go through every child of table element
360 $rowID = 1;
361 $tableColumnIndex = 1;
362 foreach ($worksheetDataSet->childNodes as $childNode) {
363 /** @var DOMElement $childNode */
364
365 // Filter elements which are not under the "table" ns
366 if ($childNode->namespaceURI != $tableNs) {
367 continue;
368 }
369
370 $key = $childNode->nodeName;
371
372 // Remove ns from node name
373 if (strpos($key, ':') !== false) {
374 $keyChunks = explode(':', $key);
375 $key = array_pop($keyChunks);
376 }
377
378 switch ($key) {
379 case 'table-header-rows':
380 /// TODO :: Figure this out. This is only a partial implementation I guess.
381 // ($rowData it's not used at all and I'm not sure that PHPExcel
382 // has an API for this)
383
384 // foreach ($rowData as $keyRowData => $cellData) {
385 // $rowData = $cellData;
386 // break;
387 // }
388 break;
389 case 'table-column':
390 if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
391 $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
392 } else {
393 $rowRepeats = 1;
394 }
395 $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
396 if (isset($columnWidths[$tableStyleName])) {
397 $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
398 $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
399 for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
400 $spreadsheet->getActiveSheet()
401 ->getColumnDimension($tableColumnString)
402 ->setWidth($columnWidth->toUnit('cm'), 'cm');
403 ++$tableColumnString;
404 }
405 }
406 $tableColumnIndex += $rowRepeats;
407
408 break;
409 case 'table-row':
410 if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
411 $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
412 } else {
413 $rowRepeats = 1;
414 }
415
416 $columnID = 'A';
417 /** @var DOMElement $cellData */
418 foreach ($childNode->childNodes as $cellData) {
419 if ($this->getReadFilter() !== null) {
420 if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
421 if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
422 $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
423 } else {
424 $colRepeats = 1;
425 }
426
427 for ($i = 0; $i < $colRepeats; ++$i) {
428 ++$columnID;
429 }
430
431 continue;
432 }
433 }
434
435 // Initialize variables
436 $formatting = $hyperlink = null;
437 $hasCalculatedValue = false;
438 $cellDataFormula = '';
439
440 if ($cellData->hasAttributeNS($tableNs, 'formula')) {
441 $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
442 $hasCalculatedValue = true;
443 }
444
445 // Annotations
446 $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
447
448 if ($annotation->length > 0 && $annotation->item(0) !== null) {
449 $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
450
451 if ($textNode->length > 0 && $textNode->item(0) !== null) {
452 $text = $this->scanElementForText($textNode->item(0));
453
454 $spreadsheet->getActiveSheet()
455 ->getComment($columnID . $rowID)
456 ->setText($this->parseRichText($text));
457 // ->setAuthor( $author )
458 }
459 }
460
461 // Content
462
463 /** @var DOMElement[] $paragraphs */
464 $paragraphs = [];
465
466 foreach ($cellData->childNodes as $item) {
467 /** @var DOMElement $item */
468
469 // Filter text:p elements
470 if ($item->nodeName == 'text:p') {
471 $paragraphs[] = $item;
472 }
473 }
474
475 if (count($paragraphs) > 0) {
476 // Consolidate if there are multiple p records (maybe with spans as well)
477 $dataArray = [];
478
479 // Text can have multiple text:p and within those, multiple text:span.
480 // text:p newlines, but text:span does not.
481 // Also, here we assume there is no text data is span fields are specified, since
482 // we have no way of knowing proper positioning anyway.
483
484 foreach ($paragraphs as $pData) {
485 $dataArray[] = $this->scanElementForText($pData);
486 }
487 $allCellDataText = implode("\n", $dataArray);
488
489 $type = $cellData->getAttributeNS($officeNs, 'value-type');
490
491 switch ($type) {
492 case 'string':
493 $type = DataType::TYPE_STRING;
494 $dataValue = $allCellDataText;
495
496 foreach ($paragraphs as $paragraph) {
497 $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
498 if ($link->length > 0 && $link->item(0) !== null) {
499 $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
500 }
501 }
502
503 break;
504 case 'boolean':
505 $type = DataType::TYPE_BOOL;
506 $dataValue = ($allCellDataText == 'TRUE') ? true : false;
507
508 break;
509 case 'percentage':
510 $type = DataType::TYPE_NUMERIC;
511 $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
512
513 // percentage should always be float
514 //if (floor($dataValue) == $dataValue) {
515 // $dataValue = (int) $dataValue;
516 //}
517 $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
518
519 break;
520 case 'currency':
521 $type = DataType::TYPE_NUMERIC;
522 $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
523
524 if (floor($dataValue) == $dataValue) {
525 $dataValue = (int) $dataValue;
526 }
527 $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
528
529 break;
530 case 'float':
531 $type = DataType::TYPE_NUMERIC;
532 $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
533
534 if (floor($dataValue) == $dataValue) {
535 if ($dataValue == (int) $dataValue) {
536 $dataValue = (int) $dataValue;
537 }
538 }
539
540 break;
541 case 'date':
542 $type = DataType::TYPE_NUMERIC;
543 $value = $cellData->getAttributeNS($officeNs, 'date-value');
544 $dataValue = Date::convertIsoDate($value);
545
546 if ($dataValue != floor($dataValue)) {
547 $formatting = NumberFormat::FORMAT_DATE_XLSX15
548 . ' '
549 . NumberFormat::FORMAT_DATE_TIME4;
550 } else {
551 $formatting = NumberFormat::FORMAT_DATE_XLSX15;
552 }
553
554 break;
555 case 'time':
556 $type = DataType::TYPE_NUMERIC;
557
558 $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
559
560 $dataValue = Date::PHPToExcel(
561 strtotime(
562 '01-01-1970 ' . implode(':', /** @scrutinizer ignore-type */ sscanf($timeValue, 'PT%dH%dM%dS') ?? [])
563 )
564 );
565 $formatting = NumberFormat::FORMAT_DATE_TIME4;
566
567 break;
568 default:
569 $dataValue = null;
570 }
571 } else {
572 $type = DataType::TYPE_NULL;
573 $dataValue = null;
574 }
575
576 if ($hasCalculatedValue) {
577 $type = DataType::TYPE_FORMULA;
578 $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
579 $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
580 }
581
582 if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
583 $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
584 } else {
585 $colRepeats = 1;
586 }
587
588 if ($type !== null) {
589 for ($i = 0; $i < $colRepeats; ++$i) {
590 if ($i > 0) {
591 ++$columnID;
592 }
593
594 if ($type !== DataType::TYPE_NULL) {
595 for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
596 $rID = $rowID + $rowAdjust;
597
598 $cell = $spreadsheet->getActiveSheet()
599 ->getCell($columnID . $rID);
600
601 // Set value
602 if ($hasCalculatedValue) {
603 $cell->setValueExplicit($cellDataFormula, $type);
604 } else {
605 $cell->setValueExplicit($dataValue, $type);
606 }
607
608 if ($hasCalculatedValue) {
609 $cell->setCalculatedValue($dataValue);
610 }
611
612 // Set other properties
613 if ($formatting !== null) {
614 $spreadsheet->getActiveSheet()
615 ->getStyle($columnID . $rID)
616 ->getNumberFormat()
617 ->setFormatCode($formatting);
618 } else {
619 $spreadsheet->getActiveSheet()
620 ->getStyle($columnID . $rID)
621 ->getNumberFormat()
622 ->setFormatCode(NumberFormat::FORMAT_GENERAL);
623 }
624
625 if ($hyperlink !== null) {
626 $cell->getHyperlink()
627 ->setUrl($hyperlink);
628 }
629 }
630 }
631 }
632 }
633
634 // Merged cells
635 $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
636
637 ++$columnID;
638 }
639 $rowID += $rowRepeats;
640
641 break;
642 }
643 }
644 $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
645 $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
646 ++$worksheetID;
647 }
648
649 $autoFilterReader->read($workbookData);
650 $definedNameReader->read($workbookData);
651 }
652 $spreadsheet->setActiveSheetIndex(0);
653
654 if ($zip->locateName('settings.xml') !== false) {
655 $this->processSettings($zip, $spreadsheet);
656 }
657
658 // Return
659 return $spreadsheet;
660 }
661
662 private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
663 {
664 $dom = new DOMDocument('1.01', 'UTF-8');
665 $dom->loadXML(
666 $this->getSecurityScannerOrThrow()
667 ->scan($zip->getFromName('settings.xml'))
668 );
669 //$xlinkNs = $dom->lookupNamespaceUri('xlink');
670 $configNs = $dom->lookupNamespaceUri('config');
671 //$oooNs = $dom->lookupNamespaceUri('ooo');
672 $officeNs = $dom->lookupNamespaceUri('office');
673 $settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
674 ->item(0);
675 if ($settings !== null) {
676 $this->lookForActiveSheet($settings, $spreadsheet, $configNs);
677 $this->lookForSelectedCells($settings, $spreadsheet, $configNs);
678 }
679 }
680
681 private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
682 {
683 /** @var DOMElement $t */
684 foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
685 if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
686 try {
687 $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? '');
688 } catch (Throwable $e) {
689 // do nothing
690 }
691
692 break;
693 }
694 }
695 }
696
697 private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
698 {
699 /** @var DOMElement $t */
700 foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
701 if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
702 foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
703 $setRow = $setCol = '';
704 $wsname = $ws->getAttributeNs($configNs, 'name');
705 foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
706 $attrName = $configItem->getAttributeNs($configNs, 'name');
707 if ($attrName === 'CursorPositionX') {
708 $setCol = $configItem->nodeValue;
709 }
710 if ($attrName === 'CursorPositionY') {
711 $setRow = $configItem->nodeValue;
712 }
713 }
714 $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
715 }
716
717 break;
718 }
719 }
720 }
721
722 private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
723 {
724 if (is_numeric($setCol) && is_numeric($setRow)) {
725 $sheet = $spreadsheet->getSheetByName($wsname);
726 if ($sheet !== null) {
727 $sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]);
728 }
729 }
730 }
731
732 /**
733 * Recursively scan element.
734 *
735 * @return string
736 */
737 protected function scanElementForText(DOMNode $element)
738 {
739 $str = '';
740 foreach ($element->childNodes as $child) {
741 /** @var DOMNode $child */
742 if ($child->nodeType == XML_TEXT_NODE) {
743 $str .= $child->nodeValue;
744 } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
745 // It's a space
746
747 // Multiple spaces?
748 $attributes = $child->attributes;
749 /** @var ?DOMAttr $cAttr */
750 $cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c');
751 $multiplier = self::getMultiplier($cAttr);
752 $str .= str_repeat(' ', $multiplier);
753 }
754
755 if ($child->hasChildNodes()) {
756 $str .= $this->scanElementForText($child);
757 }
758 }
759
760 return $str;
761 }
762
763 private static function getMultiplier(?DOMAttr $cAttr): int
764 {
765 if ($cAttr) {
766 $multiplier = (int) $cAttr->nodeValue;
767 } else {
768 $multiplier = 1;
769 }
770
771 return $multiplier;
772 }
773
774 /**
775 * @param string $is
776 *
777 * @return RichText
778 */
779 private function parseRichText($is)
780 {
781 $value = new RichText();
782 $value->createText($is);
783
784 return $value;
785 }
786
787 private function processMergedCells(
788 DOMElement $cellData,
789 string $tableNs,
790 string $type,
791 string $columnID,
792 int $rowID,
793 Spreadsheet $spreadsheet
794 ): void {
795 if (
796 $cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
797 || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
798 ) {
799 if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) {
800 $columnTo = $columnID;
801
802 if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
803 $columnIndex = Coordinate::columnIndexFromString($columnID);
804 $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
805 $columnIndex -= 2;
806
807 $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
808 }
809
810 $rowTo = $rowID;
811
812 if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
813 $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
814 }
815
816 $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
817 $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
818 }
819 }
820 }
821 }
822