Search moodle.org's
Developer Documentation

See Release Notes

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