1 |
efrain |
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 readonly 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 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 |
}
|