1<?php
2/**
3 * Zend Framework
4 *
5 * LICENSE
6 *
7 * This source file is subject to the new BSD license that is bundled
8 * with this package in the file LICENSE.txt.
9 * It is also available through the world-wide-web at this URL:
10 * http://framework.zend.com/license/new-bsd
11 * If you did not receive a copy of the license and are unable to
12 * obtain it through the world-wide-web, please send an email
13 * to license@zend.com so we can send you a copy immediately.
14 *
15 * @category   Zend
16 * @package    Zend_Filter
17 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
18 * @license    http://framework.zend.com/license/new-bsd     New BSD License
19 * @version    $Id$
20 */
21
22/**
23 * @see Zend_Filter_Compress_CompressAbstract
24 */
25
26/**
27 * Compression adapter for zip
28 *
29 * @category   Zend
30 * @package    Zend_Filter
31 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
32 * @license    http://framework.zend.com/license/new-bsd     New BSD License
33 */
34class Zend_Filter_Compress_Zip extends Zend_Filter_Compress_CompressAbstract
35{
36    /**
37     * Compression Options
38     * array(
39     *     'archive'  => Archive to use
40     *     'password' => Password to use
41     *     'target'   => Target to write the files to
42     * )
43     *
44     * @var array
45     */
46    protected $_options = array(
47        'archive' => null,
48        'target'  => null,
49    );
50
51    /**
52     * Class constructor
53     *
54     * @param string|array $options (Optional) Options to set
55     */
56    public function __construct($options = null)
57    {
58        if (!extension_loaded('zip')) {
59            throw new Zend_Filter_Exception('This filter needs the zip extension');
60        }
61        parent::__construct($options);
62    }
63
64    /**
65     * Returns the set archive
66     *
67     * @return string
68     */
69    public function getArchive()
70    {
71        return $this->_options['archive'];
72    }
73
74    /**
75     * Sets the archive to use for de-/compression
76     *
77     * @param string $archive Archive to use
78     * @return Zend_Filter_Compress_Rar
79     */
80    public function setArchive($archive)
81    {
82        $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $archive);
83        $this->_options['archive'] = (string) $archive;
84
85        return $this;
86    }
87
88    /**
89     * Returns the set targetpath
90     *
91     * @return string
92     */
93    public function getTarget()
94    {
95        return $this->_options['target'];
96    }
97
98    /**
99     * Sets the target to use
100     *
101     * @param string $target
102     * @return Zend_Filter_Compress_Rar
103     */
104    public function setTarget($target)
105    {
106        if (!file_exists(dirname($target))) {
107            throw new Zend_Filter_Exception("The directory '$target' does not exist");
108        }
109
110        $target = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $target);
111        $this->_options['target'] = (string) $target;
112        return $this;
113    }
114
115    /**
116     * Compresses the given content
117     *
118     * @param  string $content
119     * @return string Compressed archive
120     */
121    public function compress($content)
122    {
123        $zip = new ZipArchive();
124        $res = $zip->open($this->getArchive(), ZipArchive::CREATE | ZipArchive::OVERWRITE);
125
126        if ($res !== true) {
127            throw new Zend_Filter_Exception($this->_errorString($res));
128        }
129
130        if (file_exists($content)) {
131            $content  = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content));
132            $basename = substr($content, strrpos($content, DIRECTORY_SEPARATOR) + 1);
133            if (is_dir($content)) {
134                $index    = strrpos($content, DIRECTORY_SEPARATOR) + 1;
135                $content .= DIRECTORY_SEPARATOR;
136                $stack    = array($content);
137                while (!empty($stack)) {
138                    $current = array_pop($stack);
139                    $files   = array();
140
141                    $dir = dir($current);
142                    while (false !== ($node = $dir->read())) {
143                        if (($node == '.') || ($node == '..')) {
144                            continue;
145                        }
146
147                        if (is_dir($current . $node)) {
148                            array_push($stack, $current . $node . DIRECTORY_SEPARATOR);
149                        }
150
151                        if (is_file($current . $node)) {
152                            $files[] = $node;
153                        }
154                    }
155
156                    $local = substr($current, $index);
157                    $zip->addEmptyDir(substr($local, 0, -1));
158
159                    foreach ($files as $file) {
160                        $zip->addFile($current . $file, $local . $file);
161                        if ($res !== true) {
162                            throw new Zend_Filter_Exception($this->_errorString($res));
163                        }
164                    }
165                }
166            } else {
167                $res = $zip->addFile($content, $basename);
168                if ($res !== true) {
169                    throw new Zend_Filter_Exception($this->_errorString($res));
170                }
171            }
172        } else {
173            $file = $this->getTarget();
174            if (!is_dir($file)) {
175                $file = basename($file);
176            } else {
177                $file = "zip.tmp";
178            }
179
180            $res = $zip->addFromString($file, $content);
181            if ($res !== true) {
182                throw new Zend_Filter_Exception($this->_errorString($res));
183            }
184        }
185
186        $zip->close();
187        return $this->_options['archive'];
188    }
189
190    /**
191     * Decompresses the given content
192     *
193     * @param  string $content
194     * @return string
195     */
196    public function decompress($content)
197    {
198        $archive = $this->getArchive();
199        if (file_exists($content)) {
200            $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content));
201        } elseif (empty($archive) || !file_exists($archive)) {
202            throw new Zend_Filter_Exception('ZIP Archive not found');
203        }
204
205        $zip = new ZipArchive();
206        $res = $zip->open($archive);
207
208        $target = $this->getTarget();
209
210        if (!empty($target) && !is_dir($target)) {
211            $target = dirname($target);
212        }
213
214        if (!empty($target)) {
215            $target = rtrim($target, '/\\') . DIRECTORY_SEPARATOR;
216        }
217
218        if (empty($target) || !is_dir($target)) {
219            throw new Zend_Filter_Exception('No target for ZIP decompression set');
220        }
221
222        if ($res !== true) {
223            throw new Zend_Filter_Exception($this->_errorString($res));
224        }
225
226        if (version_compare(PHP_VERSION, '5.2.8', '<')) {
227            for ($i = 0; $i < $zip->numFiles; $i++) {
228                $statIndex = $zip->statIndex($i);
229                $currName = $statIndex['name'];
230                if (($currName[0] == '/') ||
231                    (substr($currName, 0, 2) == '..') ||
232                    (substr($currName, 0, 4) == './..')
233                    )
234                {
235                    throw new Zend_Filter_Exception('Upward directory traversal was detected inside ' . $archive
236                        . ' please use PHP 5.2.8 or greater to take advantage of path resolution features of '
237                        . 'the zip extension in this decompress() method.'
238                        );
239                }
240            }
241        }
242
243        $res = @$zip->extractTo($target);
244        if ($res !== true) {
245            throw new Zend_Filter_Exception($this->_errorString($res));
246        }
247
248        $zip->close();
249        return $target;
250    }
251
252    /**
253     * Returns the proper string based on the given error constant
254     *
255     * @param string $error
256     */
257    protected function _errorString($error)
258    {
259        switch($error) {
260            case ZipArchive::ER_MULTIDISK :
261                return 'Multidisk ZIP Archives not supported';
262
263            case ZipArchive::ER_RENAME :
264                return 'Failed to rename the temporary file for ZIP';
265
266            case ZipArchive::ER_CLOSE :
267                return 'Failed to close the ZIP Archive';
268
269            case ZipArchive::ER_SEEK :
270                return 'Failure while seeking the ZIP Archive';
271
272            case ZipArchive::ER_READ :
273                return 'Failure while reading the ZIP Archive';
274
275            case ZipArchive::ER_WRITE :
276                return 'Failure while writing the ZIP Archive';
277
278            case ZipArchive::ER_CRC :
279                return 'CRC failure within the ZIP Archive';
280
281            case ZipArchive::ER_ZIPCLOSED :
282                return 'ZIP Archive already closed';
283
284            case ZipArchive::ER_NOENT :
285                return 'No such file within the ZIP Archive';
286
287            case ZipArchive::ER_EXISTS :
288                return 'ZIP Archive already exists';
289
290            case ZipArchive::ER_OPEN :
291                return 'Can not open ZIP Archive';
292
293            case ZipArchive::ER_TMPOPEN :
294                return 'Failure creating temporary ZIP Archive';
295
296            case ZipArchive::ER_ZLIB :
297                return 'ZLib Problem';
298
299            case ZipArchive::ER_MEMORY :
300                return 'Memory allocation problem while working on a ZIP Archive';
301
302            case ZipArchive::ER_CHANGED :
303                return 'ZIP Entry has been changed';
304
305            case ZipArchive::ER_COMPNOTSUPP :
306                return 'Compression method not supported within ZLib';
307
308            case ZipArchive::ER_EOF :
309                return 'Premature EOF within ZIP Archive';
310
311            case ZipArchive::ER_INVAL :
312                return 'Invalid argument for ZLIB';
313
314            case ZipArchive::ER_NOZIP :
315                return 'Given file is no zip archive';
316
317            case ZipArchive::ER_INTERNAL :
318                return 'Internal error while working on a ZIP Archive';
319
320            case ZipArchive::ER_INCONS :
321                return 'Inconsistent ZIP archive';
322
323            case ZipArchive::ER_REMOVE :
324                return 'Can not remove ZIP Archive';
325
326            case ZipArchive::ER_DELETED :
327                return 'ZIP Entry has been deleted';
328
329            default :
330                return 'Unknown error within ZIP Archive';
331        }
332    }
333
334    /**
335     * Returns the adapter name
336     *
337     * @return string
338     */
339    public function toString()
340    {
341        return 'Zip';
342    }
343}
344