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]

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