Completed
Branch master (1a8c72)
by Marco
11:05
created

Zip::add()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 8
nop 2
dl 0
loc 23
rs 9.5555
c 0
b 0
f 0
1
<?php namespace Comodojo\Zip;
2
3
use \Comodojo\Foundation\Validation\DataFilter;
4
use \Comodojo\Exception\ZipException;
5
use \ZipArchive;
6
use \DirectoryIterator;
7
use \Countable;
8
9
/**
10
 * comodojo/zip - ZipArchive toolbox
11
 *
12
 * @package     Comodojo Spare Parts
13
 * @author      Marco Giovinazzi <[email protected]>
14
 * @license     MIT
15
 *
16
 * LICENSE:
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
class Zip implements Countable {
28
29
    public const SKIP_NONE = 'NONE';
30
31
    public const SKIP_HIDDEN = 'HIDDEN';
32
33
    public const SKIP_ALL = 'ALL';
34
35
    public const SKIP_COMODOJO = 'COMODOJO';
36
37
    protected const DEFAULT_MASK = 0777;
38
39
    /**
40
     * Select files to skip
41
     *
42
     * @var string
43
     */
44
    private $skip_mode = self::SKIP_NONE;
45
46
    /**
47
     * Supported skip modes
48
     *
49
     * @var bool
50
     */
51
    private $supported_skip_modes = ['NONE', 'HIDDEN', 'ALL', 'COMODOJO'];
52
53
    /**
54
     * Mask for the extraction folder (if it should be created)
55
     *
56
     * @var int
57
     */
58
    private $mask = self::DEFAULT_MASK;
59
60
    /**
61
     * ZipArchive internal pointer
62
     *
63
     * @var ZipArchive
64
     */
65
    private $zip_archive;
66
67
    /**
68
     * zip file name
69
     *
70
     * @var string
71
     */
72
    private $zip_file;
73
74
    /**
75
     * zip file password (only for extract)
76
     *
77
     * @var string
78
     */
79
    private $password;
80
81
    /**
82
     * Current base path
83
     *
84
     * @var string
85
     */
86
    private $path;
87
88
    /**
89
     * Class constructor
90
     *
91
     * @param string $zip_file ZIP file name
92
     *
93
     * @throws ZipException
94
     */
95
    public function __construct(string $zip_file) {
96
97
        if ( empty($zip_file) ) {
98
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
99
        }
100
101
        $this->zip_file = $zip_file;
102
103
    }
104
105
    /**
106
     * Open a zip archive (static constructor)
107
     *
108
     * @param string $zip_file File name
109
     *
110
     * @return Zip
111
     * @throws ZipException
112
     */
113
    public static function open(string $zip_file): Zip {
114
115
        try {
116
117
            $zip = new Zip($zip_file);
118
            $zip->setArchive(self::openZipFile($zip_file));
119
120
        } catch (ZipException $ze) {
121
            throw $ze;
122
        }
123
124
        return $zip;
125
126
    }
127
128
    /**
129
     * Check a zip archive (static constructor)
130
     *
131
     * @param string $zip_file ZIP file name
132
     *
133
     * @return bool
134
     * @throws ZipException
135
     */
136
    public static function check(string $zip_file): bool {
137
138
        try {
139
140
            $zip = self::openZipFile($zip_file, ZipArchive::CHECKCONS);
141
            $zip->close();
142
143
        } catch (ZipException $ze) {
144
            throw $ze;
145
        }
146
147
        return true;
148
149
    }
150
151
    /**
152
     * Create a new zip archive (static constructor)
153
     *
154
     * @param string $zip_file ZIP file name
155
     * @param bool $overwrite Overwrite existing file (if any)
156
     *
157
     * @return Zip
158
     * @throws ZipException
159
     */
160
    public static function create(string $zip_file, bool $overwrite = false): Zip {
161
162
        $overwrite = DataFilter::filterBoolean($overwrite);
0 ignored issues
show
Bug introduced by
$overwrite of type boolean is incompatible with the type array expected by parameter $bool of Comodojo\Foundation\Vali...Filter::filterBoolean(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
        $overwrite = DataFilter::filterBoolean(/** @scrutinizer ignore-type */ $overwrite);
Loading history...
163
164
        try {
165
166
            $zip = new Zip($zip_file);
167
168
            if ( $overwrite ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $overwrite of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
169
                $zip->setArchive(
170
                    self::openZipFile(
171
                        $zip_file,
172
                        ZipArchive::CREATE | ZipArchive::OVERWRITE
173
                    )
174
                );
175
            } else {
176
                $zip->setArchive(
177
                    self::openZipFile($zip_file, ZipArchive::CREATE)
178
                );
179
            }
180
181
        } catch (ZipException $ze) {
182
            throw $ze;
183
        }
184
185
        return $zip;
186
187
    }
188
189
    /**
190
     * Count the number of files in the archive
191
     *
192
     * @return int
193
     */
194
    public function count(): int {
195
        return count($this->zip_archive);
0 ignored issues
show
Bug introduced by
$this->zip_archive of type ZipArchive is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
        return count(/** @scrutinizer ignore-type */ $this->zip_archive);
Loading history...
196
    }
197
198
    /**
199
     * Set files to skip
200
     *
201
     * Supported skip modes:
202
     *  Zip::SKIP_NONE - skip no files
203
     *  Zip::SKIP_HIDDEN - skip hidden files
204
     *  Zip::SKIP_ALL - skip HIDDEN + COMODOJO ghost files
205
     *  Zip::SKIP_COMODOJO - skip comodojo ghost files
206
     *
207
     * @param string $mode Skip file mode
208
     *
209
     * @return  Zip
210
     * @throws  ZipException
211
     */
212
    public function setSkipped(string $mode): Zip {
213
214
        $mode = strtoupper($mode);
215
216
        if ( !in_array($mode, $this->supported_skip_modes) ) {
0 ignored issues
show
Bug introduced by
$this->supported_skip_modes of type boolean is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

216
        if ( !in_array($mode, /** @scrutinizer ignore-type */ $this->supported_skip_modes) ) {
Loading history...
217
            throw new ZipException("Unsupported skip mode: $mode");
218
        }
219
220
        $this->skip_mode = $mode;
221
222
        return $this;
223
224
    }
225
226
    /**
227
     * Get current skip mode
228
     *
229
     * @return string
230
     */
231
    public function getSkipped(): string {
232
233
        return $this->skip_mode;
234
235
    }
236
237
    /**
238
     * Set zip password
239
     *
240
     * @param string $password
241
     *
242
     * @return Zip
243
     */
244
    public function setPassword(string $password): Zip {
245
246
        $this->password = $password;
247
248
        return $this;
249
250
    }
251
252
    /**
253
     * Get current zip password
254
     *
255
     * @return string
256
     */
257
    public function getPassword(): ?string {
258
259
        return $this->password;
260
261
    }
262
263
    /**
264
     * Set current base path (to add relative files to zip archive)
265
     *
266
     * @param string|null $path
267
     *
268
     * @return Zip
269
     * @throws ZipException
270
     */
271
    public function setPath(?string $path = null): Zip {
272
273
        if ( $path === null ) {
274
            $this->path = null;
275
        } else if ( !file_exists($path) ) {
276
            throw new ZipException("Not existent path: $path");
277
        } else {
278
            $this->path = $path;
279
        }
280
281
        return $this;
282
283
    }
284
285
    /**
286
     * Get current base path
287
     *
288
     * @return string|null
289
     */
290
    public function getPath(): ?string {
291
292
        return $this->path;
293
294
    }
295
296
    /**
297
     * Set the mask of the extraction folder
298
     *
299
     * @param int $mask Integer representation of the file mask
300
     *
301
     * @return Zip
302
     */
303
    public function setMask(int $mask): Zip {
304
305
        $mask = filter_var($mask, FILTER_VALIDATE_INT, [
306
            "options" => [
307
                "max_range" => self::DEFAULT_MASK,
308
                "default" => self::DEFAULT_MASK
309
            ],
310
            'flags' => FILTER_FLAG_ALLOW_OCTAL
311
        ]);
312
        $this->mask = $mask;
313
314
        return $this;
315
316
    }
317
318
    /**
319
     * Get current mask of the extraction folder
320
     *
321
     * @return int
322
     */
323
    public function getMask(): int {
324
325
        return $this->mask;
326
327
    }
328
329
    /**
330
     * Set the current ZipArchive object
331
     *
332
     * @param ZipArchive $zip
333
     *
334
     * @return Zip
335
     */
336
    public function setArchive(ZipArchive $zip): Zip {
337
338
        $this->zip_archive = $zip;
339
340
        return $this;
341
342
    }
343
344
    /**
345
     * Get current ZipArchive object
346
     *
347
     * @return ZipArchive|null
348
     */
349
    public function getArchive(): ?ZipArchive {
350
351
        return $this->zip_archive;
352
353
    }
354
355
    /**
356
     * Get current zip file
357
     *
358
     * @return string
359
     */
360
    public function getZipFile(): string {
361
362
        return $this->zip_file;
363
364
    }
365
366
    /**
367
     * Get the list of files in the archive as an array
368
     *
369
     * @return array
370
     * @throws ZipException
371
     */
372
    public function listFiles(): array {
373
374
        $list = [];
375
376
        for ( $i = 0; $i < $this->zip_archive->numFiles; $i++ ) {
377
378
            $name = $this->zip_archive->getNameIndex($i);
379
            if ( $name === false ) {
380
                throw new ZipException(StatusCodes::get($this->zip_archive->status));
381
            }
382
            $list[] = $name;
383
384
        }
385
386
        return $list;
387
388
    }
389
390
    /**
391
     * Extract files from zip archive
392
     *
393
     * @param string $destination Destination path
394
     * @param mixed $files (optional) a filename or an array of filenames
395
     *
396
     * @return bool
397
     * @throws ZipException
398
     */
399
    public function extract(string $destination, $files = null): bool {
400
401
        if ( empty($destination) ) {
402
            throw new ZipException("Invalid destination path: $destination");
403
        }
404
405
        if ( !file_exists($destination) ) {
406
407
            $omask = umask(0);
408
            $action = mkdir($destination, $this->mask, true);
409
            umask($omask);
410
411
            if ( $action === false ) {
412
                throw new ZipException("Error creating folder: $destination");
413
            }
414
415
        }
416
417
        if ( !is_writable($destination) ) {
418
            throw new ZipException("Destination path $destination not writable");
419
        }
420
421
        if ( is_array($files) && @sizeof($files) != 0 ) {
422
            $file_matrix = $files;
423
        } else {
424
            $file_matrix = $this->getArchiveFiles();
425
        }
426
427
        if ( !empty($this->password) ) {
428
            $this->zip_archive->setPassword($this->password);
429
        }
430
431
        $extract = $this->zip_archive->extractTo($destination, $file_matrix);
432
433
        if ( $extract === false ) {
434
            throw new ZipException(StatusCodes::get($this->zip_archive->status));
435
        }
436
437
        return true;
438
439
    }
440
441
    /**
442
     * Add files to zip archive
443
     *
444
     * @param mixed $file_name_or_array Filename to add or an array of filenames
445
     * @param bool $flatten_root_folder In case of directory, specify if root folder should be flatten or not
446
     *
447
     * @return Zip
448
     * @throws ZipException
449
     */
450
    public function add($file_name_or_array, bool $flatten_root_folder = false): Zip {
451
452
        if ( empty($file_name_or_array) ) {
453
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
454
        }
455
456
        $flatten_root_folder = DataFilter::filterBoolean($flatten_root_folder);
0 ignored issues
show
Bug introduced by
$flatten_root_folder of type boolean is incompatible with the type array expected by parameter $bool of Comodojo\Foundation\Vali...Filter::filterBoolean(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

456
        $flatten_root_folder = DataFilter::filterBoolean(/** @scrutinizer ignore-type */ $flatten_root_folder);
Loading history...
457
458
        try {
459
460
            if ( is_array($file_name_or_array) ) {
461
                foreach ( $file_name_or_array as $file_name ) {
462
                    $this->addItem($file_name, $flatten_root_folder);
0 ignored issues
show
Bug introduced by
It seems like $flatten_root_folder can also be of type array; however, parameter $flatroot of Comodojo\Zip\Zip::addItem() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

462
                    $this->addItem($file_name, /** @scrutinizer ignore-type */ $flatten_root_folder);
Loading history...
463
                }
464
            } else {
465
                $this->addItem($file_name_or_array, $flatten_root_folder);
466
            }
467
468
        } catch (ZipException $ze) {
469
            throw $ze;
470
        }
471
472
        return $this;
473
474
    }
475
476
    /**
477
     * Delete files from zip archive
478
     *
479
     * @param mixed $file_name_or_array Filename to delete or an array of filenames
480
     *
481
     * @return Zip
482
     * @throws ZipException
483
     */
484
    public function delete($file_name_or_array): Zip {
485
486
        if ( empty($file_name_or_array) ) {
487
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
488
        }
489
490
        try {
491
492
            if ( is_array($file_name_or_array) ) {
493
                foreach ( $file_name_or_array as $file_name ) {
494
                    $this->deleteItem($file_name);
495
                }
496
            } else {
497
                $this->deleteItem($file_name_or_array);
498
            }
499
500
        } catch (ZipException $ze) {
501
            throw $ze;
502
        }
503
504
        return $this;
505
506
    }
507
508
    /**
509
     * Close the zip archive
510
     *
511
     * @return bool
512
     * @throws ZipException
513
     */
514
    public function close(): bool {
515
516
        if ( $this->zip_archive->close() === false ) {
517
            throw new ZipException(StatusCodes::get($this->zip_archive->status));
518
        }
519
520
        return true;
521
522
    }
523
524
    /**
525
     * Get a list of file contained in zip archive before extraction
526
     *
527
     * @return array
528
     */
529
    private function getArchiveFiles(): array {
530
531
        $list = [];
532
533
        for ( $i = 0; $i < $this->zip_archive->numFiles; $i++ ) {
534
535
            $file = $this->zip_archive->statIndex($i);
536
            if ( $file === false ) {
537
                continue;
538
            }
539
540
            $name = str_replace('\\', '/', $file['name']);
541
            if (
542
                $name[0] == "." &&
543
                in_array($this->skip_mode, ["HIDDEN", "ALL"])
544
            ) {
545
                continue;
546
            }
547
548
            if (
549
                $name[0] == "." &&
550
                @$name[1] == "_" &&
551
                in_array($this->skip_mode, ["COMODOJO", "ALL"])
552
            ) {
553
                continue;
554
            }
555
556
            $list[] = $name;
557
558
        }
559
560
        return $list;
561
562
    }
563
564
    /**
565
     * Add item to zip archive
566
     *
567
     * @param string $file File to add (realpath)
568
     * @param bool $flatroot (optional) If true, source directory will be not included
569
     * @param string $base (optional) Base to record in zip file
570
     * @return void
571
     * @throws ZipException
572
     */
573
    private function addItem(
574
        string $file,
575
        bool $flatroot = false,
576
        ?string $base = null
577
    ): void {
578
579
        $file = is_null($this->path) ? $file : $this->path."/$file";
0 ignored issues
show
introduced by
The condition is_null($this->path) is always false.
Loading history...
580
        $real_file = str_replace('\\', '/', realpath($file));
581
        $real_name = basename($real_file);
582
583
        if ( !is_null($base) ) {
584
585
            if (
586
                $real_name[0] == "." &&
587
                in_array($this->skip_mode, ["HIDDEN", "ALL"])
588
            ) {
589
                return;
590
            }
591
592
            if (
593
                $real_name[0] == "." &&
594
                @$real_name[1] == "_" &&
595
                in_array($this->skip_mode, ["COMODOJO", "ALL"])
596
            ) {
597
                return;
598
            }
599
600
        }
601
602
        if ( is_dir($real_file) ) {
603
604
            if ( !$flatroot ) {
605
606
                $folder_target = is_null($base) ? $real_name : $base.$real_name;
607
                $new_folder = $this->zip_archive->addEmptyDir($folder_target);
608
                if ( $new_folder === false ) {
609
                    throw new ZipException(StatusCodes::get($this->zip_archive->status));
610
                }
611
612
            } else {
613
                $folder_target = null;
614
            }
615
616
            foreach ( new DirectoryIterator($real_file) as $path ) {
617
618
                if ( $path->isDot() ) {
619
                    continue;
620
                }
621
622
                $file_real = $path->getPathname();
623
                $base = is_null($folder_target) ? null : ($folder_target."/");
624
625
                try {
626
                    $this->addItem($file_real, false, $base);
627
                } catch (ZipException $ze) {
628
                    throw $ze;
629
                }
630
631
            }
632
633
        } else if ( is_file($real_file) ) {
634
635
            $file_target = is_null($base) ? $real_name : $base.$real_name;
636
            $add_file = $this->zip_archive->addFile($real_file, $file_target);
637
            if ( $add_file === false ) {
638
                throw new ZipException(StatusCodes::get($this->zip_archive->status));
639
            }
640
641
        } else {
642
            return;
643
        }
644
645
    }
646
647
    /**
648
     * Delete item from zip archive
649
     *
650
     * @param string $file File to delete (zippath)
651
     * @return void
652
     * @throws ZipException
653
     */
654
    private function deleteItem(string $file): void {
655
656
        $deleted = $this->zip_archive->deleteName($file);
657
        if ( $deleted === false ) {
658
            throw new ZipException(StatusCodes::get($this->zip_archive->status));
659
        }
660
661
    }
662
663
    /**
664
     * Open a zip file
665
     *
666
     * @param string $zip_file ZIP file name
667
     * @param int $flags ZipArchive::open flags
668
     *
669
     * @return  ZipArchive
670
     * @throws  ZipException
671
     */
672
    private static function openZipFile(string $zip_file, int $flags = null): ZipArchive {
673
674
        $zip = new ZipArchive();
675
676
        $open = $zip->open($zip_file, $flags);
677
        if ( $open !== true ) {
678
            throw new ZipException(StatusCodes::get($open));
679
        }
680
681
        return $zip;
682
683
    }
684
685
}
686