Completed
Push — master ( 1a8c72...81e1ab )
by Marco
12:55
created

Zip::addFileItem()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 4
nop 3
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php namespace Comodojo\Zip;
2
3
use \Comodojo\Zip\Interfaces\ZipInterface;
4
use \Comodojo\Zip\Base\StatusCodes;
5
use \Comodojo\Zip\Traits\{
6
    SkipTrait,
7
    MaskTrait,
8
    PasswordTrait,
9
    PathTrait,
10
    ArchiveTrait
11
};
12
use \Comodojo\Foundation\Validation\DataFilter;
13
use \Comodojo\Exception\ZipException;
14
use \ZipArchive;
15
use \DirectoryIterator;
16
use \Countable;
17
18
/**
19
 * comodojo/zip - ZipArchive toolbox
20
 *
21
 * @package     Comodojo Spare Parts
22
 * @author      Marco Giovinazzi <[email protected]>
23
 * @license     MIT
24
 *
25
 * LICENSE:
26
 *
27
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
33
 * THE SOFTWARE.
34
 */
35
36
class Zip implements ZipInterface, Countable {
37
38
    use SkipTrait;
39
    use MaskTrait;
40
    use PasswordTrait;
41
    use PathTrait;
42
    use ArchiveTrait;
43
44
    /**
45
     * zip file name
46
     *
47
     * @var string
48
     */
49
    private $zip_file;
50
51
    /**
52
     * Class constructor
53
     *
54
     * @param string $zip_file ZIP file name
55
     *
56
     * @throws ZipException
57
     */
58
    public function __construct(string $zip_file) {
59
60
        if ( empty($zip_file) ) {
61
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
62
        }
63
64
        $this->zip_file = $zip_file;
65
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public static function open(string $zip_file): ZipInterface {
72
73
        try {
74
75
            $zip = new Zip($zip_file);
76
            $zip->setArchive(self::openZipFile($zip_file));
77
78
        } catch (ZipException $ze) {
79
            throw $ze;
80
        }
81
82
        return $zip;
83
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public static function check(string $zip_file): bool {
90
91
        try {
92
93
            $zip = self::openZipFile($zip_file, ZipArchive::CHECKCONS);
94
            $zip->close();
95
96
        } catch (ZipException $ze) {
97
            throw $ze;
98
        }
99
100
        return true;
101
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public static function create(string $zip_file, bool $overwrite = false): ZipInterface {
108
109
        $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

109
        $overwrite = DataFilter::filterBoolean(/** @scrutinizer ignore-type */ $overwrite);
Loading history...
110
111
        try {
112
113
            $zip = new Zip($zip_file);
114
115
            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...
116
                $zip->setArchive(
117
                    self::openZipFile(
118
                        $zip_file,
119
                        ZipArchive::CREATE | ZipArchive::OVERWRITE
120
                    )
121
                );
122
            } else {
123
                $zip->setArchive(
124
                    self::openZipFile($zip_file, ZipArchive::CREATE)
125
                );
126
            }
127
128
        } catch (ZipException $ze) {
129
            throw $ze;
130
        }
131
132
        return $zip;
133
134
    }
135
136
    /**
137
     * Count the number of files in the archive
138
     *
139
     * @return int
140
     */
141
    public function count(): int {
142
        return count($this->getArchive());
0 ignored issues
show
Bug introduced by
$this->getArchive() 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

142
        return count(/** @scrutinizer ignore-type */ $this->getArchive());
Loading history...
143
    }
144
145
    /**
146
     * Get current zip file
147
     *
148
     * @return string
149
     */
150
    public function getZipFile(): string {
151
152
        return $this->zip_file;
153
154
    }
155
156
    /**
157
     * Get the list of files in the archive as an array
158
     *
159
     * @return array
160
     * @throws ZipException
161
     */
162
    public function listFiles(): array {
163
164
        $list = [];
165
166
        for ( $i = 0; $i < $this->getArchive()->numFiles; $i++ ) {
167
168
            $name = $this->getArchive()->getNameIndex($i);
169
            if ( $name === false ) {
170
                throw new ZipException(StatusCodes::get($this->getArchive()->status));
171
            }
172
            $list[] = $name;
173
174
        }
175
176
        return $list;
177
178
    }
179
180
    /**
181
     * Extract files from zip archive
182
     *
183
     * @param string $destination Destination path
184
     * @param mixed $files (optional) a filename or an array of filenames
185
     *
186
     * @return bool
187
     * @throws ZipException
188
     */
189
    public function extract(string $destination, $files = null): bool {
190
191
        if ( empty($destination) ) {
192
            throw new ZipException("Invalid destination path: $destination");
193
        }
194
195
        if ( !file_exists($destination) ) {
196
197
            $omask = umask(0);
198
            $action = mkdir($destination, $this->getMask(), true);
199
            umask($omask);
200
201
            if ( $action === false ) {
202
                throw new ZipException("Error creating folder: $destination");
203
            }
204
205
        }
206
207
        if ( !is_writable($destination) ) {
208
            throw new ZipException("Destination path $destination not writable");
209
        }
210
211
        if ( is_array($files) && @sizeof($files) != 0 ) {
212
            $file_matrix = $files;
213
        } else {
214
            $file_matrix = $this->getArchiveFiles();
215
        }
216
217
        if ( !empty($this->getPassword()) ) {
218
            $this->getArchive()->setPassword($this->getPassword());
219
        }
220
221
        $extract = $this->getArchive()->extractTo($destination, $file_matrix);
222
223
        if ( $extract === false ) {
224
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
225
        }
226
227
        return true;
228
229
    }
230
231
    /**
232
     * Add files to zip archive
233
     *
234
     * @param mixed $file_name_or_array Filename to add or an array of filenames
235
     * @param bool $flatten_root_folder In case of directory, specify if root folder should be flatten or not
236
     *
237
     * @return Zip
238
     * @throws ZipException
239
     */
240
    public function add($file_name_or_array, bool $flatten_root_folder = false): Zip {
241
242
        if ( empty($file_name_or_array) ) {
243
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
244
        }
245
246
        $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

246
        $flatten_root_folder = DataFilter::filterBoolean(/** @scrutinizer ignore-type */ $flatten_root_folder);
Loading history...
247
248
        try {
249
250
            if ( is_array($file_name_or_array) ) {
251
                foreach ( $file_name_or_array as $file_name ) {
252
                    $this->addItem($file_name, $flatten_root_folder);
0 ignored issues
show
Bug introduced by
$flatten_root_folder of type array is incompatible with the type boolean expected by parameter $flatroot of Comodojo\Zip\Zip::addItem(). ( Ignorable by Annotation )

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

252
                    $this->addItem($file_name, /** @scrutinizer ignore-type */ $flatten_root_folder);
Loading history...
253
                }
254
            } else {
255
                $this->addItem($file_name_or_array, $flatten_root_folder);
256
            }
257
258
        } catch (ZipException $ze) {
259
            throw $ze;
260
        }
261
262
        return $this;
263
264
    }
265
266
    /**
267
     * Delete files from zip archive
268
     *
269
     * @param mixed $file_name_or_array Filename to delete or an array of filenames
270
     *
271
     * @return Zip
272
     * @throws ZipException
273
     */
274
    public function delete($file_name_or_array): Zip {
275
276
        if ( empty($file_name_or_array) ) {
277
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
278
        }
279
280
        try {
281
282
            if ( is_array($file_name_or_array) ) {
283
                foreach ( $file_name_or_array as $file_name ) {
284
                    $this->deleteItem($file_name);
285
                }
286
            } else {
287
                $this->deleteItem($file_name_or_array);
288
            }
289
290
        } catch (ZipException $ze) {
291
            throw $ze;
292
        }
293
294
        return $this;
295
296
    }
297
298
    /**
299
     * Close the zip archive
300
     *
301
     * @return bool
302
     * @throws ZipException
303
     */
304
    public function close(): bool {
305
306
        if ( $this->getArchive()->close() === false ) {
307
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
308
        }
309
310
        return true;
311
312
    }
313
314
    /**
315
     * Get a list of file contained in zip archive before extraction
316
     *
317
     * @return array
318
     */
319
    private function getArchiveFiles(): array {
320
321
        $list = [];
322
323
        for ( $i = 0; $i < $this->getArchive()->numFiles; $i++ ) {
324
325
            $file = $this->getArchive()->statIndex($i);
326
            if ( $file === false ) {
327
                continue;
328
            }
329
330
            $name = str_replace('\\', '/', $file['name']);
331
            if (
332
                (
333
                    $name[0] == "." &&
334
                    in_array($this->getSkipMode(), ["HIDDEN", "ALL"])
335
                ) ||
336
                (
337
                    $name[0] == "." &&
338
                    @$name[1] == "_" &&
339
                    in_array($this->getSkipMode(), ["COMODOJO", "ALL"])
340
                )
341
            ) {
342
                continue;
343
            }
344
345
            $list[] = $name;
346
347
        }
348
349
        return $list;
350
351
    }
352
353
    /**
354
     * Add item to zip archive
355
     *
356
     * @param string $file File to add (realpath)
357
     * @param bool $flatroot (optional) If true, source directory will be not included
358
     * @param string $base (optional) Base to record in zip file
359
     * @return void
360
     * @throws ZipException
361
     */
362
    private function addItem(
363
        string $file,
364
        bool $flatroot = false,
365
        ?string $base = null
366
    ): void {
367
368
        $file = is_null($this->getPath()) ? $file : $this->getPath()."/$file";
0 ignored issues
show
introduced by
The condition is_null($this->getPath()) is always false.
Loading history...
369
        $real_file = str_replace('\\', '/', realpath($file));
370
        $real_name = basename($real_file);
371
372
        if ( !is_null($base) ) {
373
374
            if (
375
                (
376
                    $real_name[0] == "." &&
377
                    in_array($this->getSkipMode(), ["HIDDEN", "ALL"])
378
                ) ||
379
                (
380
                    $real_name[0] == "." &&
381
                    @$real_name[1] == "_" &&
382
                    in_array($this->getSkipMode(), ["COMODOJO", "ALL"])
383
                )
384
            ) {
385
                return;
386
            }
387
388
        }
389
390
        if ( is_dir($real_file) ) {
391
            $this->addDirectoryItem($real_file, $real_name, $base, $flatroot);
392
        } else if ( is_file($real_file) ) {
393
            $this->addFileItem($real_file, $real_name, $base);
394
        } else {
395
            return;
396
        }
397
398
    }
399
400
    private function addFileItem(
401
        string $real_file,
402
        string $real_name,
403
        ?string $base = null
404
    ): void {
405
406
        $file_target = is_null($base) ? $real_name : $base.$real_name;
407
        $add_file = $this->getArchive()->addFile($real_file, $file_target);
408
        if ( $add_file === false ) {
409
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
410
        }
411
412
    }
413
414
    private function addDirectoryItem(
415
        string $real_file,
416
        string $real_name,
417
        ?string $base = null,
418
        bool $flatroot
419
    ): void {
420
421
        if ( !$flatroot ) {
422
423
            $folder_target = is_null($base) ? $real_name : $base.$real_name;
424
            $new_folder = $this->getArchive()->addEmptyDir($folder_target);
425
            if ( $new_folder === false ) {
426
                throw new ZipException(StatusCodes::get($this->getArchive()->status));
427
            }
428
429
        } else {
430
            $folder_target = null;
431
        }
432
433
        foreach ( new DirectoryIterator($real_file) as $path ) {
434
435
            if ( $path->isDot() ) {
436
                continue;
437
            }
438
439
            $file_real = $path->getPathname();
440
            $base = is_null($folder_target) ? null : ($folder_target."/");
441
442
            try {
443
                $this->addItem($file_real, false, $base);
444
            } catch (ZipException $ze) {
445
                throw $ze;
446
            }
447
448
        }
449
450
    }
451
452
    /**
453
     * Delete item from zip archive
454
     *
455
     * @param string $file File to delete (zippath)
456
     * @return void
457
     * @throws ZipException
458
     */
459
    private function deleteItem(string $file): void {
460
461
        $deleted = $this->getArchive()->deleteName($file);
462
        if ( $deleted === false ) {
463
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
464
        }
465
466
    }
467
468
    /**
469
     * Open a zip file
470
     *
471
     * @param string $zip_file ZIP file name
472
     * @param int $flags ZipArchive::open flags
473
     *
474
     * @return  ZipArchive
475
     * @throws  ZipException
476
     */
477
    private static function openZipFile(string $zip_file, int $flags = null): ZipArchive {
478
479
        $zip = new ZipArchive();
480
481
        $open = $zip->open($zip_file, $flags);
482
        if ( $open !== true ) {
483
            throw new ZipException(StatusCodes::get($open));
484
        }
485
486
        return $zip;
487
488
    }
489
490
}
491