Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 402 and 403]

   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace OpenSpout\Reader\ODS;
   6  
   7  use DOMElement;
   8  use OpenSpout\Common\Exception\IOException;
   9  use OpenSpout\Common\Helper\Escaper\ODS;
  10  use OpenSpout\Reader\Common\XMLProcessor;
  11  use OpenSpout\Reader\Exception\XMLProcessingException;
  12  use OpenSpout\Reader\ODS\Helper\CellValueFormatter;
  13  use OpenSpout\Reader\ODS\Helper\SettingsHelper;
  14  use OpenSpout\Reader\SheetIteratorInterface;
  15  use OpenSpout\Reader\Wrapper\XMLReader;
  16  
  17  /**
  18   * @implements SheetIteratorInterface<Sheet>
  19   */
  20  final class SheetIterator implements SheetIteratorInterface
  21  {
  22      public const CONTENT_XML_FILE_PATH = 'content.xml';
  23  
  24      public const XML_STYLE_NAMESPACE = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0';
  25  
  26      /**
  27       * Definition of XML nodes name and attribute used to parse sheet data.
  28       */
  29      public const XML_NODE_AUTOMATIC_STYLES = 'office:automatic-styles';
  30      public const XML_NODE_STYLE_TABLE_PROPERTIES = 'table-properties';
  31      public const XML_NODE_TABLE = 'table:table';
  32      public const XML_ATTRIBUTE_STYLE_NAME = 'style:name';
  33      public const XML_ATTRIBUTE_TABLE_NAME = 'table:name';
  34      public const XML_ATTRIBUTE_TABLE_STYLE_NAME = 'table:style-name';
  35      public const XML_ATTRIBUTE_TABLE_DISPLAY = 'table:display';
  36  
  37      /** @var string Path of the file to be read */
  38      private string $filePath;
  39  
  40      private Options $options;
  41  
  42      /** @var XMLReader The XMLReader object that will help read sheet's XML data */
  43      private XMLReader $xmlReader;
  44  
  45      /** @var ODS Used to unescape XML data */
  46      private ODS $escaper;
  47  
  48      /** @var bool Whether there are still at least a sheet to be read */
  49      private bool $hasFoundSheet;
  50  
  51      /** @var int The index of the sheet being read (zero-based) */
  52      private int $currentSheetIndex;
  53  
  54      /** @var string The name of the sheet that was defined as active */
  55      private ?string $activeSheetName;
  56  
  57      /** @var array<string, bool> Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE] */
  58      private array $sheetsVisibility;
  59  
  60      public function __construct(
  61          string $filePath,
  62          Options $options,
  63          ODS $escaper,
  64          SettingsHelper $settingsHelper
  65      ) {
  66          $this->filePath = $filePath;
  67          $this->options = $options;
  68          $this->xmlReader = new XMLReader();
  69          $this->escaper = $escaper;
  70          $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
  71      }
  72  
  73      /**
  74       * Rewind the Iterator to the first element.
  75       *
  76       * @see http://php.net/manual/en/iterator.rewind.php
  77       *
  78       * @throws \OpenSpout\Common\Exception\IOException If unable to open the XML file containing sheets' data
  79       */
  80      public function rewind(): void
  81      {
  82          $this->xmlReader->close();
  83  
  84          if (false === $this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH)) {
  85              $contentXmlFilePath = $this->filePath.'#'.self::CONTENT_XML_FILE_PATH;
  86  
  87              throw new IOException("Could not open \"{$contentXmlFilePath}\".");
  88          }
  89  
  90          try {
  91              $this->sheetsVisibility = $this->readSheetsVisibility();
  92              $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
  93          } catch (XMLProcessingException $exception) {
  94              throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
  95          }
  96  
  97          $this->currentSheetIndex = 0;
  98      }
  99  
 100      /**
 101       * Checks if current position is valid.
 102       *
 103       * @see http://php.net/manual/en/iterator.valid.php
 104       */
 105      public function valid(): bool
 106      {
 107          $valid = $this->hasFoundSheet;
 108          if (!$valid) {
 109              $this->xmlReader->close();
 110          }
 111  
 112          return $valid;
 113      }
 114  
 115      /**
 116       * Move forward to next element.
 117       *
 118       * @see http://php.net/manual/en/iterator.next.php
 119       */
 120      public function next(): void
 121      {
 122          $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
 123  
 124          if ($this->hasFoundSheet) {
 125              ++$this->currentSheetIndex;
 126          }
 127      }
 128  
 129      /**
 130       * Return the current element.
 131       *
 132       * @see http://php.net/manual/en/iterator.current.php
 133       */
 134      public function current(): Sheet
 135      {
 136          $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
 137          \assert(null !== $escapedSheetName);
 138          $sheetName = $this->escaper->unescape($escapedSheetName);
 139  
 140          $isSheetActive = $this->isSheetActive($sheetName, $this->currentSheetIndex, $this->activeSheetName);
 141  
 142          $sheetStyleName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_STYLE_NAME);
 143          \assert(null !== $sheetStyleName);
 144          $isSheetVisible = $this->isSheetVisible($sheetStyleName);
 145  
 146          return new Sheet(
 147              new RowIterator(
 148                  $this->options,
 149                  new CellValueFormatter($this->options->SHOULD_FORMAT_DATES, new ODS()),
 150                  new XMLProcessor($this->xmlReader)
 151              ),
 152              $this->currentSheetIndex,
 153              $sheetName,
 154              $isSheetActive,
 155              $isSheetVisible
 156          );
 157      }
 158  
 159      /**
 160       * Return the key of the current element.
 161       *
 162       * @see http://php.net/manual/en/iterator.key.php
 163       */
 164      public function key(): int
 165      {
 166          return $this->currentSheetIndex + 1;
 167      }
 168  
 169      /**
 170       * Extracts the visibility of the sheets.
 171       *
 172       * @return array<string, bool> Associative array [STYLE_NAME] => [IS_SHEET_VISIBLE]
 173       */
 174      private function readSheetsVisibility(): array
 175      {
 176          $sheetsVisibility = [];
 177  
 178          $this->xmlReader->readUntilNodeFound(self::XML_NODE_AUTOMATIC_STYLES);
 179  
 180          $automaticStylesNode = $this->xmlReader->expand();
 181          \assert($automaticStylesNode instanceof DOMElement);
 182  
 183          $tableStyleNodes = $automaticStylesNode->getElementsByTagNameNS(self::XML_STYLE_NAMESPACE, self::XML_NODE_STYLE_TABLE_PROPERTIES);
 184  
 185          foreach ($tableStyleNodes as $tableStyleNode) {
 186              $isSheetVisible = ('false' !== $tableStyleNode->getAttribute(self::XML_ATTRIBUTE_TABLE_DISPLAY));
 187  
 188              $parentStyleNode = $tableStyleNode->parentNode;
 189              \assert($parentStyleNode instanceof DOMElement);
 190              $styleName = $parentStyleNode->getAttribute(self::XML_ATTRIBUTE_STYLE_NAME);
 191  
 192              $sheetsVisibility[$styleName] = $isSheetVisible;
 193          }
 194  
 195          return $sheetsVisibility;
 196      }
 197  
 198      /**
 199       * Returns whether the current sheet was defined as the active one.
 200       *
 201       * @param string      $sheetName       Name of the current sheet
 202       * @param int         $sheetIndex      Index of the current sheet
 203       * @param null|string $activeSheetName Name of the sheet that was defined as active or NULL if none defined
 204       *
 205       * @return bool Whether the current sheet was defined as the active one
 206       */
 207      private function isSheetActive(string $sheetName, int $sheetIndex, ?string $activeSheetName): bool
 208      {
 209          // The given sheet is active if its name matches the defined active sheet's name
 210          // or if no information about the active sheet was found, it defaults to the first sheet.
 211          return
 212              (null === $activeSheetName && 0 === $sheetIndex)
 213              || ($activeSheetName === $sheetName)
 214          ;
 215      }
 216  
 217      /**
 218       * Returns whether the current sheet is visible.
 219       *
 220       * @param string $sheetStyleName Name of the sheet style
 221       *
 222       * @return bool Whether the current sheet is visible
 223       */
 224      private function isSheetVisible(string $sheetStyleName): bool
 225      {
 226          return $this->sheetsVisibility[$sheetStyleName] ??
 227              true;
 228      }
 229  }