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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body