Completed
Push — master ( 9d0e46...0a1061 )
by Marco
12:11
created

Zip::add()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 32
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 9
nop 4
dl 0
loc 32
rs 8.8333
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);
110
111
        try {
112
113
            $zip = new Zip($zip_file);
114
115
            if ( $overwrite ) {
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
     * {@inheritdoc}
158
     */
159
    public function listFiles(): array {
160
161
        $list = [];
162
163
        for ( $i = 0; $i < $this->getArchive()->numFiles; $i++ ) {
164
165
            $name = $this->getArchive()->getNameIndex($i);
166
            if ( $name === false ) {
167
                throw new ZipException(StatusCodes::get($this->getArchive()->status));
168
            }
169
            $list[] = $name;
170
171
        }
172
173
        return $list;
174
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function extract(string $destination, $files = null): bool {
181
182
        if ( empty($destination) ) {
183
            throw new ZipException("Invalid destination path: $destination");
184
        }
185
186
        if ( !file_exists($destination) ) {
187
188
            $omask = umask(0);
189
            $action = mkdir($destination, $this->getMask(), true);
190
            umask($omask);
191
192
            if ( $action === false ) {
193
                throw new ZipException("Error creating folder: $destination");
194
            }
195
196
        }
197
198
        if ( !is_writable($destination) ) {
199
            throw new ZipException("Destination path $destination not writable");
200
        }
201
202
        if ( is_array($files) && @sizeof($files) != 0 ) {
203
            $file_matrix = $files;
204
        } else {
205
            $file_matrix = $this->getArchiveFiles();
206
        }
207
208
        if ( !empty($this->getPassword()) ) {
209
            $this->getArchive()->setPassword($this->getPassword());
210
        }
211
212
        if ( $this->getArchive()->extractTo($destination, $file_matrix) === false ) {
213
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
214
        }
215
216
        return true;
217
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223
    public function add(
224
        $file_name_or_array,
225
        bool $flatten_root_folder = false,
226
        int $compression = self::CM_DEFAULT,
227
        int $encryption = self::EM_NONE
228
    ): ZipInterface {
229
230
        if ( empty($file_name_or_array) ) {
231
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
232
        }
233
234
        if ( $encryption !== self::EM_NONE && $this->getPassword() === null ) {
0 ignored issues
show
introduced by
The condition $this->getPassword() === null is always false.
Loading history...
235
            throw new ZipException("Cannot encrypt resource: no password set");
236
        }
237
238
        $flatten_root_folder = DataFilter::filterBoolean($flatten_root_folder);
239
240
        try {
241
242
            if ( is_array($file_name_or_array) ) {
243
                foreach ( $file_name_or_array as $file_name ) {
244
                    $this->addItem($file_name, $flatten_root_folder, $compression, $encryption);
245
                }
246
            } else {
247
                $this->addItem($file_name_or_array, $flatten_root_folder, $compression, $encryption);
248
            }
249
250
        } catch (ZipException $ze) {
251
            throw $ze;
252
        }
253
254
        return $this;
255
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function delete($file_name_or_array): ZipInterface {
262
263
        if ( empty($file_name_or_array) ) {
264
            throw new ZipException(StatusCodes::get(ZipArchive::ER_NOENT));
265
        }
266
267
        try {
268
269
            if ( is_array($file_name_or_array) ) {
270
                foreach ( $file_name_or_array as $file_name ) {
271
                    $this->deleteItem($file_name);
272
                }
273
            } else {
274
                $this->deleteItem($file_name_or_array);
275
            }
276
277
        } catch (ZipException $ze) {
278
            throw $ze;
279
        }
280
281
        return $this;
282
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288
    public function close(): bool {
289
290
        if ( $this->getArchive()->close() === false ) {
291
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
292
        }
293
294
        return true;
295
296
    }
297
298
    /**
299
     * Get a list of file contained in zip archive before extraction
300
     *
301
     * @return array
302
     */
303
    private function getArchiveFiles(): array {
304
305
        $list = [];
306
307
        for ( $i = 0; $i < $this->getArchive()->numFiles; $i++ ) {
308
309
            $file = $this->getArchive()->statIndex($i);
310
            if ( $file === false ) {
311
                continue;
312
            }
313
314
            $name = str_replace('\\', '/', $file['name']);
315
            if (
316
                (
317
                    $name[0] == "." &&
318
                    in_array($this->getSkipMode(), ["HIDDEN", "ALL"])
319
                ) ||
320
                (
321
                    $name[0] == "." &&
322
                    @$name[1] == "_" &&
323
                    in_array($this->getSkipMode(), ["COMODOJO", "ALL"])
324
                )
325
            ) {
326
                continue;
327
            }
328
329
            $list[] = $name;
330
331
        }
332
333
        return $list;
334
335
    }
336
337
    /**
338
     * Add item to zip archive
339
     *
340
     * @param string $file File to add (realpath)
341
     * @param bool $flatroot (optional) If true, source directory will be not included
342
     * @param string $base (optional) Base to record in zip file
343
     * @return void
344
     * @throws ZipException
345
     */
346
    private function addItem(
347
        string $file,
348
        bool $flatroot = false,
349
        int $compression = self::CM_DEFAULT,
350
        int $encryption = self::EM_NONE,
351
        ?string $base = null
352
    ): void {
353
354
        $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...
355
        $real_file = str_replace('\\', '/', realpath($file));
356
        $real_name = basename($real_file);
357
358
        if (
359
            $base !== null &&
360
            (
361
                (
362
                    $real_name[0] == "." &&
363
                    in_array($this->getSkipMode(), ["HIDDEN", "ALL"])
364
                ) ||
365
                (
366
                    $real_name[0] == "." &&
367
                    @$real_name[1] == "_" &&
368
                    in_array($this->getSkipMode(), ["COMODOJO", "ALL"])
369
                )
370
            )
371
        ) {
372
            return;
373
        }
374
375
        if ( is_dir($real_file) ) {
376
            $this->addDirectoryItem($real_file, $real_name, $compression, $encryption, $base, $flatroot);
377
        } else {
378
            $this->addFileItem($real_file, $real_name, $compression, $encryption, $base);
379
        }
380
381
    }
382
383
    private function addFileItem(
384
        string $real_file,
385
        string $real_name,
386
        int $compression = self::CM_DEFAULT,
387
        int $encryption = self::EM_NONE,
388
        ?string $base = null
389
    ): void {
390
391
        $file_target = is_null($base) ? $real_name : $base.$real_name;
392
        if (
393
            $this->getArchive()->addFile($real_file, $file_target) === false ||
394
            $this->getArchive()->setCompressionName($file_target, $compression) === false ||
395
            $this->getArchive()->setEncryptionName($file_target, $encryption, $this->getPassword()) === false
396
        ) {
397
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
398
        }
399
400
    }
401
402
    private function addDirectoryItem(
403
        string $real_file,
404
        string $real_name,
405
        int $compression = self::CM_DEFAULT,
406
        int $encryption = self::EM_NONE,
407
        ?string $base = null,
408
        bool $flatroot = false
409
    ): void {
410
411
        if ( !$flatroot ) {
412
            $folder_target = $base.$real_name;
413
            $new_base = "$folder_target/";
414
            if ( $this->getArchive()->addEmptyDir($folder_target) === false ) {
415
                throw new ZipException(StatusCodes::get($this->getArchive()->status));
416
            }
417
        } else {
418
            $new_base = null;
419
        }
420
421
        foreach ( new DirectoryIterator($real_file) as $path ) {
422
423
            if ( $path->isDot() ) {
424
                continue;
425
            }
426
427
            try {
428
                $this->addItem(
429
                    $path->getPathname(),
430
                    false,
431
                    $compression,
432
                    $encryption,
433
                    $new_base
434
                );
435
            } catch (ZipException $ze) {
436
                throw $ze;
437
            }
438
439
        }
440
441
    }
442
443
    /**
444
     * Delete item from zip archive
445
     *
446
     * @param string $file File to delete (zippath)
447
     * @return void
448
     * @throws ZipException
449
     */
450
    private function deleteItem(string $file): void {
451
452
        if ( $this->getArchive()->deleteName($file) === false ) {
453
            throw new ZipException(StatusCodes::get($this->getArchive()->status));
454
        }
455
456
    }
457
458
    /**
459
     * Open a zip file
460
     *
461
     * @param string $zip_file ZIP file name
462
     * @param int $flags ZipArchive::open flags
463
     *
464
     * @return  ZipArchive
465
     * @throws  ZipException
466
     */
467
    private static function openZipFile(string $zip_file, int $flags = null): ZipArchive {
468
469
        $zip = new ZipArchive();
470
471
        $open = $zip->open($zip_file, $flags);
472
        if ( $open !== true ) {
473
            throw new ZipException(StatusCodes::get($open));
474
        }
475
476
        return $zip;
477
478
    }
479
480
}
481