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\XLSX\Manager;
   6  
   7  use OpenSpout\Common\Exception\IOException;
   8  use OpenSpout\Reader\Wrapper\XMLReader;
   9  
  10  /**
  11   * @internal
  12   */
  13  final class WorkbookRelationshipsManager
  14  {
  15      public const BASE_PATH = 'xl/';
  16  
  17      /**
  18       * Path of workbook relationships XML file inside the XLSX file.
  19       */
  20      public const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels';
  21  
  22      /**
  23       * Relationships types - For Transitional and Strict OOXML.
  24       */
  25      public const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
  26      public const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
  27      public const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings';
  28      public const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles';
  29  
  30      /**
  31       * Nodes and attributes used to find relevant information in the workbook relationships XML file.
  32       */
  33      public const XML_NODE_RELATIONSHIP = 'Relationship';
  34      public const XML_ATTRIBUTE_TYPE = 'Type';
  35      public const XML_ATTRIBUTE_TARGET = 'Target';
  36  
  37      /** @var string Path of the XLSX file being read */
  38      private string $filePath;
  39  
  40      /** @var array<string, string> Cache of the already read workbook relationships: [TYPE] => [FILE_NAME] */
  41      private array $cachedWorkbookRelationships;
  42  
  43      /**
  44       * @param string $filePath Path of the XLSX file being read
  45       */
  46      public function __construct(string $filePath)
  47      {
  48          $this->filePath = $filePath;
  49      }
  50  
  51      /**
  52       * @return string The path of the shared string XML file
  53       */
  54      public function getSharedStringsXMLFilePath(): string
  55      {
  56          $workbookRelationships = $this->getWorkbookRelationships();
  57          $sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]
  58              ?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT];
  59  
  60          // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
  61          $doesContainBasePath = str_contains($sharedStringsXMLFilePath, self::BASE_PATH);
  62          if (!$doesContainBasePath) {
  63              // make sure we return an absolute file path
  64              $sharedStringsXMLFilePath = self::BASE_PATH.$sharedStringsXMLFilePath;
  65          }
  66  
  67          return $sharedStringsXMLFilePath;
  68      }
  69  
  70      /**
  71       * @return bool Whether the XLSX file contains a shared string XML file
  72       */
  73      public function hasSharedStringsXMLFile(): bool
  74      {
  75          $workbookRelationships = $this->getWorkbookRelationships();
  76  
  77          return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS])
  78              || isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]);
  79      }
  80  
  81      /**
  82       * @return bool Whether the XLSX file contains a styles XML file
  83       */
  84      public function hasStylesXMLFile(): bool
  85      {
  86          $workbookRelationships = $this->getWorkbookRelationships();
  87  
  88          return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES])
  89              || isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]);
  90      }
  91  
  92      /**
  93       * @return string The path of the styles XML file
  94       */
  95      public function getStylesXMLFilePath(): string
  96      {
  97          $workbookRelationships = $this->getWorkbookRelationships();
  98          $stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]
  99              ?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT];
 100  
 101          // the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
 102          $doesContainBasePath = str_contains($stylesXMLFilePath, self::BASE_PATH);
 103          if (!$doesContainBasePath) {
 104              // make sure we return a full path
 105              $stylesXMLFilePath = self::BASE_PATH.$stylesXMLFilePath;
 106          }
 107  
 108          return $stylesXMLFilePath;
 109      }
 110  
 111      /**
 112       * Reads the workbook.xml.rels and extracts the filename associated to the different types.
 113       * It caches the result so that the file is read only once.
 114       *
 115       * @return array<string, string>
 116       *
 117       * @throws \OpenSpout\Common\Exception\IOException If workbook.xml.rels can't be read
 118       */
 119      private function getWorkbookRelationships(): array
 120      {
 121          if (!isset($this->cachedWorkbookRelationships)) {
 122              $xmlReader = new XMLReader();
 123  
 124              if (false === $xmlReader->openFileInZip($this->filePath, self::WORKBOOK_RELS_XML_FILE_PATH)) {
 125                  throw new IOException('Could not open "'.self::WORKBOOK_RELS_XML_FILE_PATH.'".');
 126              }
 127  
 128              $this->cachedWorkbookRelationships = [];
 129  
 130              while ($xmlReader->readUntilNodeFound(self::XML_NODE_RELATIONSHIP)) {
 131                  $this->processWorkbookRelationship($xmlReader);
 132              }
 133          }
 134  
 135          return $this->cachedWorkbookRelationships;
 136      }
 137  
 138      /**
 139       * Extracts and store the data of the current workbook relationship.
 140       */
 141      private function processWorkbookRelationship(XMLReader $xmlReader): void
 142      {
 143          $type = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TYPE);
 144          $target = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
 145          \assert(null !== $target);
 146  
 147          // @NOTE: if a type is defined more than once, we overwrite the previous value
 148          // To be changed if we want to get the file paths of sheet XML files for instance.
 149          $this->cachedWorkbookRelationships[$type] = $target;
 150      }
 151  }