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