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