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\Wrapper;
   4  
   5  /**
   6   * Class XMLReader
   7   * Wrapper around the built-in XMLReader
   8   * @see \XMLReader
   9   */
  10  class XMLReader extends \XMLReader
  11  {
  12      use XMLInternalErrorsHelper;
  13  
  14      const ZIP_WRAPPER = 'zip://';
  15  
  16      /**
  17       * Opens the XML Reader to read a file located inside a ZIP file.
  18       *
  19       * @param string $zipFilePath Path to the ZIP file
  20       * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip
  21       * @return bool TRUE on success or FALSE on failure
  22       */
  23      public function openFileInZip($zipFilePath, $fileInsideZipPath)
  24      {
  25          $wasOpenSuccessful = false;
  26          $realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath);
  27  
  28          // We need to check first that the file we are trying to read really exist because:
  29          //  - PHP emits a warning when trying to open a file that does not exist.
  30          //  - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779)
  31          if ($this->fileExistsWithinZip($realPathURI)) {
  32              $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET);
  33          }
  34  
  35          return $wasOpenSuccessful;
  36      }
  37  
  38      /**
  39       * Returns the real path for the given path components.
  40       * This is useful to avoid issues on some Windows setup.
  41       *
  42       * @param string $zipFilePath Path to the ZIP file
  43       * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip
  44       * @return string The real path URI
  45       */
  46      public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
  47      {
  48          // The file path should not start with a '/', otherwise it won't be found
  49          $fileInsideZipPathWithoutLeadingSlash = \ltrim($fileInsideZipPath, '/');
  50  
  51          return (self::ZIP_WRAPPER . \realpath($zipFilePath) . '#' . $fileInsideZipPathWithoutLeadingSlash);
  52      }
  53  
  54      /**
  55       * Returns whether the file at the given location exists
  56       *
  57       * @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml"
  58       * @return bool TRUE if the file exists, FALSE otherwise
  59       */
  60      protected function fileExistsWithinZip($zipStreamURI)
  61      {
  62          $doesFileExists = false;
  63  
  64          $pattern = '/zip:\/\/([^#]+)#(.*)/';
  65          if (\preg_match($pattern, $zipStreamURI, $matches)) {
  66              $zipFilePath = $matches[1];
  67              $innerFilePath = $matches[2];
  68  
  69              $zip = new \ZipArchive();
  70              if ($zip->open($zipFilePath) === true) {
  71                  $doesFileExists = ($zip->locateName($innerFilePath) !== false);
  72                  $zip->close();
  73              }
  74          }
  75  
  76          return $doesFileExists;
  77      }
  78  
  79      /**
  80       * Move to next node in document
  81       * @see \XMLReader::read
  82       *
  83       * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
  84       * @return bool TRUE on success or FALSE on failure
  85       */
  86      #[\ReturnTypeWillChange]
  87      public function read()
  88      {
  89          $this->useXMLInternalErrors();
  90  
  91          $wasReadSuccessful = parent::read();
  92  
  93          $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
  94  
  95          return $wasReadSuccessful;
  96      }
  97  
  98      /**
  99       * Read until the element with the given name is found, or the end of the file.
 100       *
 101       * @param string $nodeName Name of the node to find
 102       * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
 103       * @return bool TRUE on success or FALSE on failure
 104       */
 105      public function readUntilNodeFound($nodeName)
 106      {
 107          do {
 108              $wasReadSuccessful = $this->read();
 109              $isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName);
 110          } while ($wasReadSuccessful && $isNotPositionedOnStartingNode);
 111  
 112          return $wasReadSuccessful;
 113      }
 114  
 115      /**
 116       * Move cursor to next node skipping all subtrees
 117       * @see \XMLReader::next
 118       *
 119       * @param string|null $localName The name of the next node to move to
 120       * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
 121       * @return bool TRUE on success or FALSE on failure
 122       */
 123      #[\ReturnTypeWillChange]
 124      public function next($localName = null)
 125      {
 126          $this->useXMLInternalErrors();
 127  
 128          $wasNextSuccessful = parent::next($localName);
 129  
 130          $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
 131  
 132          return $wasNextSuccessful;
 133      }
 134  
 135      /**
 136       * @param string $nodeName
 137       * @return bool Whether the XML Reader is currently positioned on the starting node with given name
 138       */
 139      public function isPositionedOnStartingNode($nodeName)
 140      {
 141          return $this->isPositionedOnNode($nodeName, self::ELEMENT);
 142      }
 143  
 144      /**
 145       * @param string $nodeName
 146       * @return bool Whether the XML Reader is currently positioned on the ending node with given name
 147       */
 148      public function isPositionedOnEndingNode($nodeName)
 149      {
 150          return $this->isPositionedOnNode($nodeName, self::END_ELEMENT);
 151      }
 152  
 153      /**
 154       * @param string $nodeName
 155       * @param int $nodeType
 156       * @return bool Whether the XML Reader is currently positioned on the node with given name and type
 157       */
 158      private function isPositionedOnNode($nodeName, $nodeType)
 159      {
 160          // In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>").
 161          // So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName").
 162          // @see https://github.com/box/spout/issues/233
 163          $hasPrefix = (\strpos($nodeName, ':') !== false);
 164          $currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
 165  
 166          return ($this->nodeType === $nodeType && $currentNodeName === $nodeName);
 167      }
 168  
 169      /**
 170       * @return string The name of the current node, un-prefixed
 171       */
 172      public function getCurrentNodeName()
 173      {
 174          return $this->localName;
 175      }
 176  }