Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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