Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.31% covered (success)
98.31%
116 / 118
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZipArchive
98.31% covered (success)
98.31%
116 / 118
91.67% covered (success)
91.67%
11 / 12
42
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 __call
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 open
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 close
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 extractTo
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getFromName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 pclzipAddFile
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
5.02
 pclzipAddFromString
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 pclzipExtractTo
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 pclzipGetFromName
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 pclzipGetNameIndex
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 pclzipLocateName
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2/**
3 * This file is part of PHPWord - A pure PHP library for reading and writing
4 * word processing documents.
5 *
6 * PHPWord is free software distributed under the terms of the GNU Lesser
7 * General Public License version 3 as published by the Free Software Foundation.
8 *
9 * For the full copyright and license information, please read the LICENSE
10 * file that was distributed with this source code. For the full list of
11 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
12 *
13 * @see         https://github.com/PHPOffice/PHPWord
14 *
15 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16 */
17
18namespace PhpOffice\PhpWord\Shared;
19
20use PclZip;
21use PhpOffice\PhpWord\Exception\Exception;
22use PhpOffice\PhpWord\Settings;
23use Throwable;
24
25/**
26 * ZipArchive wrapper.
27 *
28 * Wraps zip archive functionality of PHP ZipArchive and PCLZip. PHP ZipArchive
29 * properties and methods are bypassed and used as the model for the PCLZip
30 * emulation. Only needed PHP ZipArchive features are implemented.
31 *
32 * @method  bool addFile(string $filename, string $localname = null)
33 * @method  bool addFromString(string $localname, string $contents)
34 * @method  false|string getNameIndex(int $index)
35 * @method  false|int locateName(string $name)
36 *
37 * @since   0.10.0
38 */
39class ZipArchive
40{
41    /** @const int Flags for open method */
42    const CREATE = 1; // Emulate \ZipArchive::CREATE
43    const OVERWRITE = 8; // Emulate \ZipArchive::OVERWRITE
44
45    /**
46     * Number of files (emulate ZipArchive::$numFiles).
47     *
48     * @var int
49     */
50    public $numFiles = 0;
51
52    /**
53     * Archive filename (emulate ZipArchive::$filename).
54     *
55     * @var string
56     */
57    public $filename;
58
59    /**
60     * Temporary storage directory.
61     *
62     * @var string
63     */
64    private $tempDir;
65
66    /**
67     * Internal zip archive object.
68     *
69     * @var PclZip|\ZipArchive
70     */
71    private $zip;
72
73    /**
74     * Use PCLZip (default behaviour).
75     *
76     * @var bool
77     */
78    private $usePclzip = true;
79
80    /**
81     * Create new instance.
82     */
83    public function __construct()
84    {
85        $this->usePclzip = (Settings::getZipClass() != 'ZipArchive');
86        if ($this->usePclzip) {
87            if (!defined('PCLZIP_TEMPORARY_DIR')) {
88                define('PCLZIP_TEMPORARY_DIR', Settings::getTempDir() . '/');
89            }
90            require_once 'PCLZip/pclzip.lib.php';
91        }
92    }
93
94    /**
95     * Catch function calls: pass to ZipArchive or PCLZip.
96     *
97     * `call_user_func_array` can only used for public function, hence the `public` in all `pcl...` methods
98     *
99     * @param mixed $function
100     * @param mixed $args
101     *
102     * @return mixed
103     */
104    public function __call($function, $args)
105    {
106        // Set object and function
107        $zipFunction = $function;
108        if (!$this->usePclzip) {
109            $zipObject = $this->zip;
110        } else {
111            $zipObject = $this;
112            $zipFunction = "pclzip{$zipFunction}";
113        }
114
115        // Run function
116        $result = false;
117        if (method_exists($zipObject, $zipFunction)) {
118            $result = @call_user_func_array([$zipObject, $zipFunction], $args);
119        }
120
121        return $result;
122    }
123
124    /**
125     * Open a new zip archive.
126     *
127     * @param string $filename The file name of the ZIP archive to open
128     * @param int $flags The mode to use to open the archive
129     *
130     * @return bool
131     */
132    public function open($filename, $flags = null)
133    {
134        $result = true;
135        $this->filename = $filename;
136        $this->tempDir = Settings::getTempDir();
137
138        if (!$this->usePclzip) {
139            $zip = new \ZipArchive();
140
141            // PHP 8.1 compat - passing null as second arg to \ZipArchive::open() is deprecated
142            // passing 0 achieves the same behaviour
143            if ($flags === null) {
144                $flags = 0;
145            }
146
147            $result = $zip->open($this->filename, $flags);
148
149            // Scrutizer will report the property numFiles does not exist
150            // See https://github.com/scrutinizer-ci/php-analyzer/issues/190
151            $this->numFiles = $zip->numFiles;
152        } else {
153            $zip = new PclZip($this->filename);
154            $zipContent = $zip->listContent();
155            $this->numFiles = is_array($zipContent) ? count($zipContent) : 0;
156        }
157        $this->zip = $zip;
158
159        return $result;
160    }
161
162    /**
163     * Close the active archive.
164     *
165     * @return bool
166     */
167    public function close()
168    {
169        if (!$this->usePclzip) {
170            try {
171                $result = @$this->zip->close();
172            } catch (Throwable $e) {
173                $result = false;
174            }
175            if ($result === false) {
176                throw new Exception("Could not close zip file {$this->filename}");
177            }
178        }
179
180        return true;
181    }
182
183    /**
184     * Extract the archive contents (emulate \ZipArchive).
185     *
186     * @param string $destination
187     * @param array|string $entries
188     *
189     * @return bool
190     *
191     * @since 0.10.0
192     */
193    public function extractTo($destination, $entries = null)
194    {
195        if (!is_dir($destination)) {
196            return false;
197        }
198
199        if (!$this->usePclzip) {
200            return $this->zip->extractTo($destination, $entries);
201        }
202
203        return $this->pclzipExtractTo($destination, $entries);
204    }
205
206    /**
207     * Extract file from archive by given file name (emulate \ZipArchive).
208     *
209     * @param  string $filename Filename for the file in zip archive
210     *
211     * @return string $contents File string contents
212     */
213    public function getFromName($filename)
214    {
215        if (!$this->usePclzip) {
216            $contents = $this->zip->getFromName($filename);
217            if ($contents === false) {
218                $filename = substr($filename, 1);
219                $contents = $this->zip->getFromName($filename);
220            }
221        } else {
222            $contents = $this->pclzipGetFromName($filename);
223        }
224
225        return $contents;
226    }
227
228    /**
229     * Add a new file to the zip archive (emulate \ZipArchive).
230     *
231     * @param string $filename Directory/Name of the file to add to the zip archive
232     * @param string $localname Directory/Name of the file added to the zip
233     *
234     * @return bool
235     */
236    public function pclzipAddFile($filename, $localname = null)
237    {
238        /** @var PclZip $zip Type hint */
239        $zip = $this->zip;
240
241        // Bugfix GH-261 https://github.com/PHPOffice/PHPWord/pull/261
242        $realpathFilename = realpath($filename);
243        if ($realpathFilename !== false) {
244            $filename = $realpathFilename;
245        }
246
247        $filenameParts = pathinfo($filename);
248        $localnameParts = pathinfo($localname);
249
250        // To Rename the file while adding it to the zip we
251        //   need to create a temp file with the correct name
252        $tempFile = false;
253        if ($filenameParts['basename'] != $localnameParts['basename']) {
254            $tempFile = true; // temp file created
255            $temppath = $this->tempDir . DIRECTORY_SEPARATOR . $localnameParts['basename'];
256            copy($filename, $temppath);
257            $filename = $temppath;
258            $filenameParts = pathinfo($temppath);
259        }
260
261        $pathRemoved = $filenameParts['dirname'];
262        $pathAdded = $localnameParts['dirname'];
263
264        if (!$this->usePclzip) {
265            $pathAdded = $pathAdded . '/' . ltrim(str_replace('\\', '/', substr($filename, strlen($pathRemoved))), '/');
266            //$res = $zip->addFile($filename, $pathAdded);
267            $res = $zip->addFromString($pathAdded, file_get_contents($filename));       // addFile can't use subfolders in some cases
268        } else {
269            $res = $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $pathRemoved, PCLZIP_OPT_ADD_PATH, $pathAdded);
270        }
271
272        if ($tempFile) {
273            // Remove temp file, if created
274            unlink($this->tempDir . DIRECTORY_SEPARATOR . $localnameParts['basename']);
275        }
276
277        return $res != 0;
278    }
279
280    /**
281     * Add a new file to the zip archive from a string of raw data (emulate \ZipArchive).
282     *
283     * @param string $localname Directory/Name of the file to add to the zip archive
284     * @param string $contents String of data to add to the zip archive
285     *
286     * @return bool
287     */
288    public function pclzipAddFromString($localname, $contents)
289    {
290        /** @var PclZip $zip Type hint */
291        $zip = $this->zip;
292        $filenameParts = pathinfo($localname);
293
294        // Write $contents to a temp file
295        $handle = fopen($this->tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename'], 'wb');
296        fwrite($handle, $contents);
297        fclose($handle);
298
299        // Add temp file to zip
300        $filename = $this->tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename'];
301        $pathRemoved = $this->tempDir;
302        $pathAdded = $filenameParts['dirname'];
303
304        $res = $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $pathRemoved, PCLZIP_OPT_ADD_PATH, $pathAdded);
305
306        // Remove temp file
307        @unlink($this->tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename']);
308
309        return $res != 0;
310    }
311
312    /**
313     * Extract the archive contents (emulate \ZipArchive).
314     *
315     * @param string $destination
316     * @param array|string $entries
317     *
318     * @return bool
319     *
320     * @since 0.10.0
321     */
322    public function pclzipExtractTo($destination, $entries = null)
323    {
324        /** @var PclZip $zip Type hint */
325        $zip = $this->zip;
326
327        // Extract all files
328        if (null === $entries) {
329            $result = $zip->extract(PCLZIP_OPT_PATH, $destination);
330
331            return $result > 0;
332        }
333
334        // Extract by entries
335        if (!is_array($entries)) {
336            $entries = [$entries];
337        }
338        foreach ($entries as $entry) {
339            $entryIndex = $this->locateName($entry);
340            $result = $zip->extractByIndex($entryIndex, PCLZIP_OPT_PATH, $destination);
341            if ($result <= 0) {
342                return false;
343            }
344        }
345
346        return true;
347    }
348
349    /**
350     * Extract file from archive by given file name (emulate \ZipArchive).
351     *
352     * @param  string $filename Filename for the file in zip archive
353     *
354     * @return string $contents File string contents
355     */
356    public function pclzipGetFromName($filename)
357    {
358        /** @var PclZip $zip Type hint */
359        $zip = $this->zip;
360        $listIndex = $this->pclzipLocateName($filename);
361        $contents = false;
362
363        if ($listIndex !== false) {
364            $extracted = $zip->extractByIndex($listIndex, PCLZIP_OPT_EXTRACT_AS_STRING);
365        } else {
366            $filename = substr($filename, 1);
367            $listIndex = $this->pclzipLocateName($filename);
368            $extracted = $zip->extractByIndex($listIndex, PCLZIP_OPT_EXTRACT_AS_STRING);
369        }
370        if ((is_array($extracted)) && ($extracted != 0)) {
371            $contents = $extracted[0]['content'];
372        }
373
374        return $contents;
375    }
376
377    /**
378     * Returns the name of an entry using its index (emulate \ZipArchive).
379     *
380     * @param int $index
381     *
382     * @return bool|string
383     *
384     * @since 0.10.0
385     */
386    public function pclzipGetNameIndex($index)
387    {
388        /** @var PclZip $zip Type hint */
389        $zip = $this->zip;
390        $list = $zip->listContent();
391        if (isset($list[$index])) {
392            return $list[$index]['filename'];
393        }
394
395        return false;
396    }
397
398    /**
399     * Returns the index of the entry in the archive (emulate \ZipArchive).
400     *
401     * @param string $filename Filename for the file in zip archive
402     *
403     * @return false|int
404     */
405    public function pclzipLocateName($filename)
406    {
407        /** @var PclZip $zip Type hint */
408        $zip = $this->zip;
409        $list = $zip->listContent();
410        $listCount = count($list);
411        $listIndex = -1;
412        for ($i = 0; $i < $listCount; ++$i) {
413            if (strtolower($list[$i]['filename']) == strtolower($filename) ||
414                strtolower($list[$i]['stored_filename']) == strtolower($filename)) {
415                $listIndex = $i;
416
417                break;
418            }
419        }
420
421        return ($listIndex > -1) ? $listIndex : false;
422    }
423}