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