Passed
Push — master ( abec97...4a93fc )
by Marco
03:29 queued 10s
created

Zip::add()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 7.7305

Importance

Changes 0
Metric Value
cc 6
eloc 11
c 0
b 0
f 0
nc 4
nop 4
dl 0
loc 26
ccs 7
cts 11
cp 0.6364
crap 7.7305
rs 9.2222
1
<?php
2
3
namespace Comodojo\Zip;
4
5
use \Comodojo\Zip\Interfaces\ZipInterface;
6
use \Comodojo\Zip\Base\StatusCodes;
7
use \Comodojo\Zip\Traits\{
8
    SkipTrait,
9
    MaskTrait,
10
    PasswordTrait,
11
    PathTrait,
12
    ArchiveTrait,
13
    CommentTrait
14
};
15
use \Comodojo\Foundation\Validation\DataFilter;
16
use \Comodojo\Exception\ZipException;
17
use \ZipArchive;
18
use \DirectoryIterator;
19
use \Countable;
20
21
/**
22
 * comodojo/zip - ZipArchive toolbox
23
 *
24
 * @package     Comodojo Spare Parts
25
 * @author      Marco Giovinazzi <[email protected]>
26
 * @license     MIT
27
 *
28
 * LICENSE:
29
 *
30
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
36
 * THE SOFTWARE.
37
 */
38
39
class Zip implements ZipInterface, Countable
40
{
41
42
    use SkipTrait;
43
    use MaskTrait;
44
    use PasswordTrait;
45
    use PathTrait;
46
    use ArchiveTrait;
47
    use CommentTrait;
48
49
    /**
50
     * zip file name
51
     *
52
     * @var string
53
     */
54
    private ?string $zip_file = null;
55
56
    /**
57
     * Class constructor
58
     *
59
     * @param string $zip_file ZIP file name
60
     *
61
     * @throws ZipException
62
     */
63 30
    public function __construct(string $zip_file)
64
    {
65 30
        if (empty($zip_file)) {
66
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
67
        }
68
69 30
        $this->zip_file = $zip_file;
70 30
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 11
    public static function open(string $zip_file): ZipInterface
76
    {
77 11
        $zip = new Zip($zip_file);
78 11
        $zip->setArchive(self::openZipFile($zip_file));
79
80 11
        return $zip;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 1
    public static function check(string $zip_file): bool
87
    {
88 1
        $zip = self::openZipFile($zip_file, ZipArchive::CHECKCONS);
89 1
        $zip->close();
90
91 1
        return true;
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 16
    public static function create(string $zip_file, bool $overwrite = false): ZipInterface
98
    {
99 16
        $overwrite = DataFilter::filterBoolean($overwrite);
100
101 16
        $zip = new Zip($zip_file);
102
103 16
        if ($overwrite) {
104
            $zip->setArchive(
105
                self::openZipFile(
106
                    $zip_file,
107
                    ZipArchive::CREATE | ZipArchive::OVERWRITE
108
                )
109
            );
110
        } else {
111 16
            $zip->setArchive(
112 16
                self::openZipFile($zip_file, ZipArchive::CREATE)
113
            );
114
        }
115
116 16
        return $zip;
117
    }
118
119
    /**
120
     * Count the number of files in the archive
121
     *
122
     * @return int
123
     */
124 1
    public function count(): int
125
    {
126 1
        return count(
127
            /** @scrutinizer ignore-type */
128 1
            $this->getArchive()
129
        );
130
    }
131
132
    /**
133
     * Get current zip file
134
     *
135
     * @return string
136
     */
137 3
    public function getZipFile(): string
138
    {
139 3
        return $this->zip_file;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 2
    public function listFiles(): array
146
    {
147 2
        $list = [];
148
149 2
        for ($i = 0; $i < $this->getArchive()->numFiles; $i++) {
150
151 2
            $name = $this->getArchive()->getNameIndex($i);
152 2
            if ($name === false) {
153
                throw new ZipException(StatusCodes::get($this->getArchive()->status));
154
            }
155 2
            $list[] = $name;
156
        }
157
158 2
        return $list;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 4
    public function extract(string $destination, $files = null): bool
165
    {
166 4
        if (empty($destination)) {
167
            throw new ZipException("Invalid destination path: $destination");
168
        }
169
170 4
        if (!file_exists($destination)) {
171 4
            $omask = umask(0);
172 4
            $action = mkdir($destination, $this->getMask(), true);
173 4
            umask($omask);
174
175 4
            if ($action === false) {
176
                throw new ZipException("Error creating folder: $destination");
177
            }
178
        }
179
180 4
        if (!is_writable($destination)) {
181
            throw new ZipException("Destination path $destination not writable");
182
        }
183
184 4
        if (is_array($files) && @sizeof($files) != 0) {
185
            $file_matrix = $files;
186
        } else {
187 4
            $file_matrix = $this->getArchiveFiles();
188
        }
189
190 4
        if ($this->getArchive()->extractTo($destination, $file_matrix) === false) {
191
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
192
        }
193
194 4
        return true;
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 15
    public function add(
201
        $file_name_or_array,
202
        bool $flatten_root_folder = false,
203
        int $compression = self::CM_DEFAULT,
204
        int $encryption = self::EM_NONE
205
    ): ZipInterface {
206
207 15
        if (empty($file_name_or_array)) {
208
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
209
        }
210
211 15
        if ($encryption !== self::EM_NONE && $this->getPassword() === null) {
212
            throw new ZipException("Cannot encrypt resource: no password set");
213
        }
214
215 15
        $flatten_root_folder = DataFilter::filterBoolean($flatten_root_folder);
216
217 15
        if (is_array($file_name_or_array)) {
218
            foreach ($file_name_or_array as $file_name) {
219
                $this->addItem($file_name, $flatten_root_folder, $compression, $encryption);
220
            }
221
        } else {
222 15
            $this->addItem($file_name_or_array, $flatten_root_folder, $compression, $encryption);
223
        }
224
225 15
        return $this;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 2
    public function delete($file_name_or_array): ZipInterface
232
    {
233 2
        if (empty($file_name_or_array)) {
234
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
235
        }
236
237 2
        if (is_array($file_name_or_array)) {
238
            foreach ($file_name_or_array as $file_name) {
239
                $this->deleteItem($file_name);
240
            }
241
        } else {
242 2
            $this->deleteItem($file_name_or_array);
243
        }
244
245 2
        return $this;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 19
    public function close(): bool
252
    {
253 19
        if ($this->getArchive()->close() === false) {
254
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
255
        }
256
257 19
        return true;
258
    }
259
260
    /**
261
     * Get a list of file contained in zip archive before extraction
262
     *
263
     * @return array
264
     */
265 4
    private function getArchiveFiles(): array
266
    {
267 4
        $list = [];
268
269 4
        for ($i = 0; $i < $this->getArchive()->numFiles; $i++) {
270
271 4
            $file = $this->getArchive()->statIndex($i);
272 4
            if ($file === false) {
273
                continue;
274
            }
275
276 4
            $name = str_replace('\\', '/', $file['name']);
277
            if (
278 4
                ($name[0] == "." &&
279
                    in_array($this->getSkipMode(), ["HIDDEN", "ALL"])) ||
280 4
                ($name[0] == "." &&
281 4
                    @$name[1] == "_" &&
282 4
                    in_array($this->getSkipMode(), ["COMODOJO", "ALL"]))
283
            ) {
284
                continue;
285
            }
286
287 4
            $list[] = $name;
288
        }
289
290 4
        return $list;
291
    }
292
293
    /**
294
     * Add item to zip archive
295
     *
296
     * @param string $file File to add (realpath)
297
     * @param bool $flatroot (optional) If true, source directory will be not included
298
     * @param string $base (optional) Base to record in zip file
299
     * @return void
300
     * @throws ZipException
301
     */
302 15
    private function addItem(
303
        string $file,
304
        bool $flatroot = false,
305
        int $compression = self::CM_DEFAULT,
306
        int $encryption = self::EM_NONE,
307
        ?string $base = null
308
    ): void {
309
310 15
        $file = is_null($this->getPath()) ? $file : $this->getPath() . "/$file";
311 15
        $real_file = str_replace('\\', '/', realpath($file));
312 15
        $real_name = basename($real_file);
313
314
        if (
315 15
            $base !== null &&
316
            (
317 4
                ($real_name[0] == "." &&
318
                    in_array($this->getSkipMode(), ["HIDDEN", "ALL"])) ||
319 4
                ($real_name[0] == "." &&
320 4
                    @$real_name[1] == "_" &&
321 15
                    in_array($this->getSkipMode(), ["COMODOJO", "ALL"])))
322
        ) {
323
            return;
324
        }
325
326 15
        if (is_dir($real_file)) {
327 4
            $this->addDirectoryItem($real_file, $real_name, $compression, $encryption, $base, $flatroot);
328
        } else {
329 15
            $this->addFileItem($real_file, $real_name, $compression, $encryption, $base);
330
        }
331 15
    }
332
333 15
    private function addFileItem(
334
        string $real_file,
335
        string $real_name,
336
        int $compression = self::CM_DEFAULT,
337
        int $encryption = self::EM_NONE,
338
        ?string $base = null
339
    ): void {
340
341 15
        $file_target = is_null($base) ? $real_name : $base . $real_name;
342
        if (
343 15
            $this->getArchive()->addFile($real_file, $file_target) === false ||
344 15
            $this->getArchive()->setCompressionName($file_target, $compression) === false ||
345 15
            $this->getArchive()->setEncryptionName($file_target, $encryption) === false
346
        ) {
347
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
348
        }
349 15
    }
350
351 4
    private function addDirectoryItem(
352
        string $real_file,
353
        string $real_name,
354
        int $compression = self::CM_DEFAULT,
355
        int $encryption = self::EM_NONE,
356
        ?string $base = null,
357
        bool $flatroot = false
358
    ): void {
359
360 4
        if (!$flatroot) {
361 4
            $folder_target = $base . $real_name;
362 4
            $new_base = "$folder_target/";
363 4
            if ($this->getArchive()->addEmptyDir($folder_target) === false) {
364 4
                throw new ZipException(StatusCodes::get($this->getArchive()->status));
365
            }
366
        } else {
367 2
            $new_base = null;
368
        }
369
370 4
        foreach (new DirectoryIterator($real_file) as $path) {
371
            
372 4
            if ($path->isDot()) {
373 4
                continue;
374
            }
375
376 4
            $this->addItem(
377 4
                $path->getPathname(),
378 4
                false,
379 4
                $compression,
380 4
                $encryption,
381 4
                $new_base
382
            );
383
        }
384 4
    }
385
386
    /**
387
     * Delete item from zip archive
388
     *
389
     * @param string $file File to delete (zippath)
390
     * @return void
391
     * @throws ZipException
392
     */
393 2
    private function deleteItem(string $file): void
394
    {
395 2
        if ($this->getArchive()->deleteName($file) === false) {
396
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
397
        }
398 2
    }
399
400
    /**
401
     * Open a zip file
402
     *
403
     * @param string $zip_file ZIP file name
404
     * @param int $flags ZipArchive::open flags
405
     *
406
     * @return  ZipArchive
407
     * @throws  ZipException
408
     */
409 27
    private static function openZipFile(string $zip_file, int $flags = null): ZipArchive
410
    {
411 27
        $zip = new ZipArchive();
412
413 27
        $open = $zip->open($zip_file, $flags);
414 27
        if ($open !== true) {
415
            throw new ZipException(StatusCodes::get($open));
416
        }
417
418 27
        return $zip;
419
    }
420
}
421