Passed
Branch master (25d5dd)
by Alexey
02:30
created

ZipFile::next()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/** @noinspection AdditionOperationOnArraysInspection */
4
5
/** @noinspection PhpUsageOfSilenceOperatorInspection */
6
7
namespace PhpZip;
8
9
use PhpZip\Constants\ZipCompressionLevel;
10
use PhpZip\Constants\ZipCompressionMethod;
11
use PhpZip\Constants\ZipEncryptionMethod;
12
use PhpZip\Constants\ZipOptions;
13
use PhpZip\Constants\ZipPlatform;
14
use PhpZip\Exception\InvalidArgumentException;
15
use PhpZip\Exception\ZipEntryNotFoundException;
16
use PhpZip\Exception\ZipException;
17
use PhpZip\IO\Stream\ResponseStream;
18
use PhpZip\IO\Stream\ZipEntryStreamWrapper;
19
use PhpZip\IO\ZipReader;
20
use PhpZip\IO\ZipWriter;
21
use PhpZip\Model\Data\ZipFileData;
22
use PhpZip\Model\Data\ZipNewData;
23
use PhpZip\Model\ZipContainer;
24
use PhpZip\Model\ZipEntry;
25
use PhpZip\Model\ZipEntryMatcher;
26
use PhpZip\Model\ZipInfo;
27
use PhpZip\Util\FilesUtil;
28
use PhpZip\Util\StringUtil;
29
use Psr\Http\Message\ResponseInterface;
30
use Symfony\Component\Finder\Finder;
31
use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo;
32
33
/**
34
 * Create, open .ZIP files, modify, get info and extract files.
35
 *
36
 * Implemented support traditional PKWARE encryption and WinZip AES encryption.
37
 * Implemented support ZIP64.
38
 * Support ZipAlign functional.
39
 *
40
 * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
41
 *
42
 * @author Ne-Lexa [email protected]
43
 * @license MIT
44
 */
45
class ZipFile implements ZipFileInterface
0 ignored issues
show
Deprecated Code introduced by
The interface PhpZip\ZipFileInterface has been deprecated: will be removed in version 4.0. Use the {@see ZipFile} class. ( Ignorable by Annotation )

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

45
class ZipFile implements /** @scrutinizer ignore-deprecated */ ZipFileInterface

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

Loading history...
46
{
47
    /** @var array default mime types */
48
    private static $defaultMimeTypes = [
49
        'zip' => 'application/zip',
50
        'apk' => 'application/vnd.android.package-archive',
51
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
52
        'epub' => 'application/epub+zip',
53
        'jar' => 'application/java-archive',
54
        'odt' => 'application/vnd.oasis.opendocument.text',
55
        'pptx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
56
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
57
        'xpi' => 'application/x-xpinstall',
58
    ];
59
60
    /** @var ZipContainer */
61
    protected $zipContainer;
62
63
    /** @var ZipReader|null */
64
    private $reader;
65
66
    /**
67
     * ZipFile constructor.
68
     */
69 174
    public function __construct()
70
    {
71 174
        $this->zipContainer = new ZipContainer();
72 174
    }
73
74
    /**
75
     * @param resource $inputStream
76
     * @param array    $options
77
     *
78
     * @return ZipReader
79
     */
80 97
    protected function createZipReader($inputStream, array $options = [])
81
    {
82 97
        return new ZipReader($inputStream, $options);
83
    }
84
85
    /**
86
     * @return ZipWriter
87
     */
88 80
    protected function createZipWriter()
89
    {
90 80
        return new ZipWriter($this->zipContainer);
91
    }
92
93
    /**
94
     * Open zip archive from file.
95
     *
96
     * @param string $filename
97
     * @param array  $options
98
     *
99
     * @throws ZipException if can't open file
100
     *
101
     * @return ZipFile
102
     */
103 79
    public function openFile($filename, array $options = [])
104
    {
105 79
        if (!file_exists($filename)) {
106 1
            throw new ZipException("File {$filename} does not exist.");
107
        }
108
109 78
        if (!($handle = @fopen($filename, 'rb'))) {
110 1
            throw new ZipException("File {$filename} can't open.");
111
        }
112
113 77
        return $this->openFromStream($handle, $options);
114
    }
115
116
    /**
117
     * Open zip archive from raw string data.
118
     *
119
     * @param string $data
120
     * @param array  $options
121
     *
122
     * @throws ZipException if can't open temp stream
123
     *
124
     * @return ZipFile
125
     */
126 8
    public function openFromString($data, array $options = [])
127
    {
128 8
        if ($data === null || $data === '') {
129 2
            throw new InvalidArgumentException('Empty string passed');
130
        }
131
132 6
        if (!($handle = fopen('php://temp', 'r+b'))) {
133
            throw new ZipException("Can't open temp stream.");
134
        }
135 6
        fwrite($handle, $data);
136 6
        rewind($handle);
137
138 6
        return $this->openFromStream($handle, $options);
139
    }
140
141
    /**
142
     * Open zip archive from stream resource.
143
     *
144
     * @param resource $handle
145
     * @param array    $options
146
     *
147
     * @throws ZipException
148
     *
149
     * @return ZipFile
150
     */
151 97
    public function openFromStream($handle, array $options = [])
152
    {
153 97
        $this->reader = $this->createZipReader($handle, $options);
154 90
        $this->zipContainer = new ZipContainer($this->reader->read());
155
156 84
        return $this;
157
    }
158
159
    /**
160
     * @return string[] returns the list files
161
     */
162 9
    public function getListFiles()
163
    {
164
        // strval is needed to cast entry names to string type
165 9
        return array_map('strval', array_keys($this->zipContainer->getEntries()));
166
    }
167
168
    /**
169
     * @return int returns the number of entries in this ZIP file
170
     */
171 39
    public function count()
172
    {
173 39
        return $this->zipContainer->count();
174
    }
175
176
    /**
177
     * Returns the file comment.
178
     *
179
     * @return string|null the file comment
180
     */
181 3
    public function getArchiveComment()
182
    {
183 3
        return $this->zipContainer->getArchiveComment();
184
    }
185
186
    /**
187
     * Set archive comment.
188
     *
189
     * @param string|null $comment
190
     *
191
     * @return ZipFile
192
     */
193 4
    public function setArchiveComment($comment = null)
194
    {
195 4
        $this->zipContainer->setArchiveComment($comment);
196
197 3
        return $this;
198
    }
199
200
    /**
201
     * Checks if there is an entry in the archive.
202
     *
203
     * @param string $entryName
204
     *
205
     * @return bool
206
     */
207 42
    public function hasEntry($entryName)
208
    {
209 42
        return $this->zipContainer->hasEntry($entryName);
210
    }
211
212
    /**
213
     * Returns ZipEntry object.
214
     *
215
     * @param string $entryName
216
     *
217
     * @throws ZipEntryNotFoundException
218
     *
219
     * @return ZipEntry
220
     */
221 17
    public function getEntry($entryName)
222
    {
223 17
        return $this->zipContainer->getEntry($entryName);
224
    }
225
226
    /**
227
     * Checks that the entry in the archive is a directory.
228
     * Returns true if and only if this ZIP entry represents a directory entry
229
     * (i.e. end with '/').
230
     *
231
     * @param string $entryName
232
     *
233
     * @throws ZipEntryNotFoundException
234
     *
235
     * @return bool
236
     */
237 1
    public function isDirectory($entryName)
238
    {
239 1
        return $this->getEntry($entryName)->isDirectory();
240
    }
241
242
    /**
243
     * Returns entry comment.
244
     *
245
     * @param string $entryName
246
     *
247
     * @throws ZipException
248
     * @throws ZipEntryNotFoundException
249
     *
250
     * @return string
251
     */
252 1
    public function getEntryComment($entryName)
253
    {
254 1
        return $this->getEntry($entryName)->getComment();
255
    }
256
257
    /**
258
     * Set entry comment.
259
     *
260
     * @param string      $entryName
261
     * @param string|null $comment
262
     *
263
     * @throws ZipEntryNotFoundException
264
     * @throws ZipException
265
     *
266
     * @return ZipFile
267
     */
268 4
    public function setEntryComment($entryName, $comment = null)
269
    {
270 4
        $this->getEntry($entryName)->setComment($comment);
271
272 2
        return $this;
273
    }
274
275
    /**
276
     * Returns the entry contents.
277
     *
278
     * @param string $entryName
279
     *
280
     * @throws ZipEntryNotFoundException
281
     * @throws ZipException
282
     *
283
     * @return string
284
     */
285 48
    public function getEntryContents($entryName)
286
    {
287 48
        $zipData = $this->zipContainer->getEntry($entryName)->getData();
288
289 47
        if ($zipData === null) {
290
            throw new ZipException(sprintf('No data for zip entry %s', $entryName));
291
        }
292
293 47
        return $zipData->getDataAsString();
294
    }
295
296
    /**
297
     * @param string $entryName
298
     *
299
     * @throws ZipEntryNotFoundException
300
     * @throws ZipException
301
     *
302
     * @return resource
303
     */
304 4
    public function getEntryStream($entryName)
305
    {
306 4
        $resource = ZipEntryStreamWrapper::wrap($this->zipContainer->getEntry($entryName));
307 4
        rewind($resource);
308
309 4
        return $resource;
310
    }
311
312
    /**
313
     * Get info by entry.
314
     *
315
     * @param string|ZipEntry $entryName
316
     *
317
     * @throws ZipException
318
     * @throws ZipEntryNotFoundException
319
     *
320
     * @return ZipInfo
321
     */
322 16
    public function getEntryInfo($entryName)
323
    {
324 16
        return new ZipInfo($this->zipContainer->getEntry($entryName));
0 ignored issues
show
Deprecated Code introduced by
The class PhpZip\Model\ZipInfo has been deprecated: Use ZipEntry ( Ignorable by Annotation )

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

324
        return /** @scrutinizer ignore-deprecated */ new ZipInfo($this->zipContainer->getEntry($entryName));
Loading history...
325
    }
326
327
    /**
328
     * Get info by all entries.
329
     *
330
     * @return ZipInfo[]
331
     */
332 10
    public function getAllInfo()
333
    {
334 10
        $infoMap = [];
335
336 10
        foreach ($this->zipContainer->getEntries() as $name => $entry) {
337 10
            $infoMap[$name] = new ZipInfo($entry);
0 ignored issues
show
Deprecated Code introduced by
The class PhpZip\Model\ZipInfo has been deprecated: Use ZipEntry ( Ignorable by Annotation )

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

337
            $infoMap[$name] = /** @scrutinizer ignore-deprecated */ new ZipInfo($entry);
Loading history...
338
        }
339
340 10
        return $infoMap;
341
    }
342
343
    /**
344
     * @return ZipEntryMatcher
345
     */
346 6
    public function matcher()
347
    {
348 6
        return $this->zipContainer->matcher();
349
    }
350
351
    /**
352
     * Returns an array of zip records (ex. for modify time).
353
     *
354
     * @return ZipEntry[] array of raw zip entries
355
     */
356 1
    public function getEntries()
357
    {
358 1
        return $this->zipContainer->getEntries();
359
    }
360
361
    /**
362
     * Extract the archive contents (unzip).
363
     *
364
     * Extract the complete archive or the given files to the specified destination.
365
     *
366
     * @param string            $destDir location where to extract the files
367
     * @param array|string|null $entries The entries to extract. It accepts either
368
     *                                   a single entry name or an array of names.
369
     *
370
     * @throws ZipException
371
     *
372
     * @return ZipFile
373
     */
374 10
    public function extractTo($destDir, $entries = null)
375
    {
376 10
        if (!file_exists($destDir)) {
377 1
            throw new ZipException(sprintf('Destination %s not found', $destDir));
378
        }
379
380 9
        if (!is_dir($destDir)) {
381 1
            throw new ZipException('Destination is not directory');
382
        }
383
384 8
        if (!is_writable($destDir)) {
385 1
            throw new ZipException('Destination is not writable directory');
386
        }
387
388 7
        $extractedEntries = [];
389
390 7
        $zipEntries = $this->zipContainer->getEntries();
391
392 7
        if (!empty($entries)) {
393 2
            if (\is_string($entries)) {
394 1
                $entries = (array) $entries;
395
            }
396
397 2
            if (\is_array($entries)) {
0 ignored issues
show
introduced by
The condition is_array($entries) is always true.
Loading history...
398 2
                $entries = array_unique($entries);
399 2
                $zipEntries = array_intersect_key($zipEntries, array_flip($entries));
400
            }
401
        }
402
403 7
        if (empty($zipEntries)) {
404 1
            return $this;
405
        }
406
407
        /** @var int[] $lastModDirs */
408 6
        $lastModDirs = [];
409
410 6
        krsort($zipEntries, \SORT_NATURAL);
411
412 6
        $symlinks = [];
413 6
        $destDir = rtrim($destDir, '/\\');
414
415 6
        foreach ($zipEntries as $entryName => $entry) {
416 6
            $unixMode = $entry->getUnixMode();
417 6
            $entryName = FilesUtil::normalizeZipPath($entryName);
418 6
            $file = $destDir . \DIRECTORY_SEPARATOR . $entryName;
419
420 6
            if (\DIRECTORY_SEPARATOR === '\\') {
421
                $file = str_replace('/', '\\', $file);
422
            }
423 6
            $extractedEntries[$file] = $entry;
424 6
            $modifyTimestamp = $entry->getMTime()->getTimestamp();
425 6
            $atime = $entry->getATime();
426 6
            $accessTimestamp = $atime === null ? null : $atime->getTimestamp();
427
428 6
            $dir = $entry->isDirectory() ? $file : \dirname($file);
429
430 6
            if (!is_dir($dir)) {
431 4
                $dirMode = $entry->isDirectory() ? $unixMode : 0755;
432
433 4
                if ($dirMode === 0) {
434
                    $dirMode = 0755;
435
                }
436
437 4
                if (!mkdir($dir, $dirMode, true) && !is_dir($dir)) {
438
                    throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir));
439
                }
440 4
                chmod($dir, $dirMode);
441
            }
442
443 6
            $parts = explode('/', rtrim($entryName, '/'));
444 6
            $path = $destDir . \DIRECTORY_SEPARATOR;
445
446 6
            foreach ($parts as $part) {
447 6
                if (!isset($lastModDirs[$path]) || $lastModDirs[$path] > $modifyTimestamp) {
448 6
                    $lastModDirs[$path] = $modifyTimestamp;
449
                }
450
451 6
                $path .= $part . \DIRECTORY_SEPARATOR;
452
            }
453
454 6
            if ($entry->isDirectory()) {
455 3
                $lastModDirs[$dir] = $modifyTimestamp;
456
457 3
                continue;
458
            }
459
460 5
            $zipData = $entry->getData();
461
462 5
            if ($zipData === null) {
463
                continue;
464
            }
465
466 5
            if ($entry->isUnixSymlink()) {
467
                $symlinks[$file] = $zipData->getDataAsString();
468
469
                continue;
470
            }
471
472
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
473 5
            if (!($handle = @fopen($file, 'w+b'))) {
474
                throw new ZipException(
475
                    sprintf(
476
                        'Cannot extract zip entry %s. File %s cannot open for write.',
477
                        $entry->getName(),
478
                        $file
479
                    )
480
                );
481
            }
482
483
            try {
484 5
                $zipData->copyDataToStream($handle);
485 1
            } catch (ZipException $e) {
486 1
                unlink($file);
487
488 1
                throw $e;
489 4
            } finally {
490 5
                fclose($handle);
491
            }
492
493 4
            if ($unixMode === 0) {
494
                $unixMode = 0644;
495
            }
496 4
            chmod($file, $unixMode);
497
498 4
            if ($accessTimestamp !== null) {
499
                /** @noinspection PotentialMalwareInspection */
500
                touch($file, $modifyTimestamp, $accessTimestamp);
501
            } else {
502 4
                touch($file, $modifyTimestamp);
503
            }
504
        }
505
506 5
        foreach ($symlinks as $linkPath => $target) {
507
            if (!FilesUtil::symlink($target, $linkPath)) {
508
                unset($extractedEntries[$linkPath]);
509
            }
510
        }
511
512 5
        krsort($lastModDirs, \SORT_NATURAL);
513
514 5
        foreach ($lastModDirs as $dir => $lastMod) {
515 5
            touch($dir, $lastMod);
516
        }
517
518
//        ksort($extractedEntries);
519
520 5
        return $this;
521
    }
522
523
    /**
524
     * Add entry from the string.
525
     *
526
     * @param string   $entryName         zip entry name
527
     * @param string   $contents          string contents
528
     * @param int|null $compressionMethod Compression method.
529
     *                                    Use {@see ZipCompressionMethod::STORED},
530
     *                                    {@see ZipCompressionMethod::DEFLATED} or
531
     *                                    {@see ZipCompressionMethod::BZIP2}.
532
     *                                    If null, then auto choosing method.
533
     *
534
     * @throws ZipException
535
     *
536
     * @return ZipFile
537
     */
538 64
    public function addFromString($entryName, $contents, $compressionMethod = null)
539
    {
540 64
        if ($entryName === null) {
0 ignored issues
show
introduced by
The condition $entryName === null is always false.
Loading history...
541 1
            throw new InvalidArgumentException('Entry name is null');
542
        }
543
544 63
        if ($contents === null) {
0 ignored issues
show
introduced by
The condition $contents === null is always false.
Loading history...
545 1
            throw new InvalidArgumentException('Contents is null');
546
        }
547
548 62
        $entryName = ltrim((string) $entryName, '\\/');
549
550 62
        if ($entryName === '') {
551 1
            throw new InvalidArgumentException('Empty entry name');
552
        }
553 61
        $contents = (string) $contents;
554 61
        $length = \strlen($contents);
555
556 61
        if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) {
557 45
            if ($length < 512) {
558 44
                $compressionMethod = ZipCompressionMethod::STORED;
559
            } else {
560 3
                $mimeType = FilesUtil::getMimeTypeFromString($contents);
561 3
                $compressionMethod = FilesUtil::isBadCompressionMimeType($mimeType) ?
562
                    ZipCompressionMethod::STORED :
563 3
                    ZipCompressionMethod::DEFLATED;
564
            }
565
        }
566
567 61
        $zipEntry = new ZipEntry($entryName);
568 61
        $zipEntry->setData(new ZipNewData($zipEntry, $contents));
569 61
        $zipEntry->setUncompressedSize($length);
570 61
        $zipEntry->setCompressionMethod($compressionMethod);
571 60
        $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
572 60
        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
573 60
        $zipEntry->setUnixMode(0100644);
574 60
        $zipEntry->setTime(time());
575
576 60
        $this->addZipEntry($zipEntry);
577
578 60
        return $this;
579
    }
580
581
    /**
582
     * @param Finder $finder
583
     * @param array  $options
584
     *
585
     * @throws ZipException
586
     *
587
     * @return ZipEntry[]
588
     */
589 1
    public function addFromFinder(Finder $finder, array $options = [])
590
    {
591
        $defaultOptions = [
592 1
            ZipOptions::STORE_ONLY_FILES => false,
593 1
            ZipOptions::COMPRESSION_METHOD => null,
594 1
            ZipOptions::MODIFIED_TIME => null,
595
        ];
596 1
        $options += $defaultOptions;
597
598 1
        if ($options[ZipOptions::STORE_ONLY_FILES]) {
599
            $finder->files();
600
        }
601
602 1
        $entries = [];
603
604 1
        foreach ($finder as $fileInfo) {
605 1
            if ($fileInfo->isReadable()) {
606 1
                $entry = $this->addSplFile($fileInfo, null, $options);
607 1
                $entries[$entry->getName()] = $entry;
608
            }
609
        }
610
611 1
        return $entries;
612
    }
613
614
    /**
615
     * @param \SplFileInfo $file
616
     * @param string|null  $entryName
617
     * @param array        $options
618
     *
619
     * @throws ZipException
620
     *
621
     * @return ZipEntry
622
     */
623 36
    public function addSplFile(\SplFileInfo $file, $entryName = null, array $options = [])
624
    {
625 36
        if ($file instanceof \DirectoryIterator) {
626
            throw new InvalidArgumentException('File should not be \DirectoryIterator.');
627
        }
628
        $defaultOptions = [
629 36
            ZipOptions::COMPRESSION_METHOD => null,
630 36
            ZipOptions::MODIFIED_TIME => null,
631
        ];
632 36
        $options += $defaultOptions;
633
634 36
        if (!$file->isReadable()) {
635 2
            throw new InvalidArgumentException(sprintf('File %s is not readable', $file->getPathname()));
636
        }
637
638 34
        if ($entryName === null) {
639 3
            if ($file instanceof SymfonySplFileInfo) {
640 1
                $entryName = $file->getRelativePathname();
641
            } else {
642 2
                $entryName = $file->getBasename();
643
            }
644
        }
645
646 34
        $entryName = ltrim((string) $entryName, '\\/');
647
648 34
        if ($entryName === '') {
649
            throw new InvalidArgumentException('Empty entry name');
650
        }
651
652 34
        $entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName;
653
654 34
        $zipEntry = new ZipEntry($entryName);
655 34
        $zipData = null;
656
657 34
        if ($file->isFile()) {
658 34
            if (isset($options[ZipOptions::COMPRESSION_METHOD])) {
659 2
                $compressionMethod = $options[ZipOptions::COMPRESSION_METHOD];
660 32
            } elseif ($file->getSize() < 512) {
661 26
                $compressionMethod = ZipCompressionMethod::STORED;
662
            } else {
663 11
                $compressionMethod = FilesUtil::isBadCompressionFile($file->getPathname()) ?
664
                    ZipCompressionMethod::STORED :
665 11
                    ZipCompressionMethod::DEFLATED;
666
            }
667
668 34
            $zipEntry->setUncompressedSize($file->getSize());
669 34
            $zipEntry->setCompressionMethod($compressionMethod);
670
671 33
            $zipData = new ZipFileData($file);
672
        } elseif ($file->isDir()) {
673
            $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
674
            $zipEntry->setUncompressedSize(0);
675
            $zipEntry->setCompressedSize(0);
676
            $zipEntry->setCrc(0);
677
        } elseif ($file->isLink()) {
678
            $linkTarget = $file->getLinkTarget();
679
            $lengthLinkTarget = \strlen($linkTarget);
680
681
            $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
682
            $zipEntry->setUncompressedSize($lengthLinkTarget);
683
            $zipEntry->setCompressedSize($lengthLinkTarget);
684
            $zipEntry->setCrc(crc32($linkTarget));
685
686
            $zipData = new ZipNewData($zipEntry, $linkTarget);
687
        }
688
689 33
        $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
690 33
        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
691 33
        $zipEntry->setUnixMode($file->getPerms());
692
693 33
        $timestamp = null;
694
695 33
        if (isset($options[ZipOptions::MODIFIED_TIME])) {
696
            $mtime = $options[ZipOptions::MODIFIED_TIME];
697
698
            if ($mtime instanceof \DateTimeInterface) {
699
                $timestamp = $mtime->getTimestamp();
700
            } elseif (is_numeric($mtime)) {
701
                $timestamp = (int) $mtime;
702
            } elseif (\is_string($mtime)) {
703
                $timestamp = strtotime($mtime);
704
705
                if ($timestamp === false) {
706
                    $timestamp = null;
707
                }
708
            }
709
        }
710
711 33
        if ($timestamp === null) {
712 33
            $timestamp = $file->getMTime();
713
        }
714
715 33
        $zipEntry->setTime($timestamp);
716 33
        $zipEntry->setData($zipData);
717
718 33
        $this->addZipEntry($zipEntry);
719
720 33
        return $zipEntry;
721
    }
722
723
    /**
724
     * @param ZipEntry $zipEntry
725
     */
726 91
    protected function addZipEntry(ZipEntry $zipEntry)
727
    {
728 91
        $this->zipContainer->addEntry($zipEntry);
729 91
    }
730
731
    /**
732
     * Add entry from the file.
733
     *
734
     * @param string      $filename          destination file
735
     * @param string|null $entryName         zip Entry name
736
     * @param int|null    $compressionMethod Compression method.
737
     *                                       Use {@see ZipCompressionMethod::STORED},
738
     *                                       {@see ZipCompressionMethod::DEFLATED} or
739
     *                                       {@see ZipCompressionMethod::BZIP2}.
740
     *                                       If null, then auto choosing method.
741
     *
742
     * @throws ZipException
743
     *
744
     * @return ZipFile
745
     */
746 35
    public function addFile($filename, $entryName = null, $compressionMethod = null)
747
    {
748 35
        if ($filename === null) {
0 ignored issues
show
introduced by
The condition $filename === null is always false.
Loading history...
749 1
            throw new InvalidArgumentException('Filename is null');
750
        }
751
752 34
        $this->addSplFile(
753 34
            new \SplFileInfo($filename),
754
            $entryName,
755
            [
756 34
                ZipOptions::COMPRESSION_METHOD => $compressionMethod,
757
            ]
758
        );
759
760 31
        return $this;
761
    }
762
763
    /**
764
     * Add entry from the stream.
765
     *
766
     * @param resource $stream            stream resource
767
     * @param string   $entryName         zip Entry name
768
     * @param int|null $compressionMethod Compression method.
769
     *                                    Use {@see ZipCompressionMethod::STORED},
770
     *                                    {@see ZipCompressionMethod::DEFLATED} or
771
     *                                    {@see ZipCompressionMethod::BZIP2}.
772
     *                                    If null, then auto choosing method.
773
     *
774
     * @throws ZipException
775
     *
776
     * @return ZipFile
777
     */
778 7
    public function addFromStream($stream, $entryName, $compressionMethod = null)
779
    {
780 7
        if (!\is_resource($stream)) {
781 1
            throw new InvalidArgumentException('Stream is not resource');
782
        }
783
784 6
        if ($entryName === null) {
0 ignored issues
show
introduced by
The condition $entryName === null is always false.
Loading history...
785
            throw new InvalidArgumentException('Entry name is null');
786
        }
787 6
        $entryName = ltrim((string) $entryName, '\\/');
788
789 6
        if ($entryName === '') {
790 1
            throw new InvalidArgumentException('Empty entry name');
791
        }
792 5
        $fstat = fstat($stream);
793
794 5
        $zipEntry = new ZipEntry($entryName);
795
796 5
        if ($fstat !== false) {
797 4
            $unixMode = $fstat['mode'];
798 4
            $length = $fstat['size'];
799
800 4
            if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) {
801 3
                if ($length < 512) {
802 1
                    $compressionMethod = ZipCompressionMethod::STORED;
803
                } else {
804 3
                    rewind($stream);
805 3
                    $bufferContents = stream_get_contents($stream, min(1024, $length));
806 3
                    rewind($stream);
807 3
                    $mimeType = FilesUtil::getMimeTypeFromString($bufferContents);
808 3
                    $compressionMethod = FilesUtil::isBadCompressionMimeType($mimeType) ?
809
                        ZipCompressionMethod::STORED :
810 3
                        ZipCompressionMethod::DEFLATED;
811
                }
812 4
                $zipEntry->setUncompressedSize($length);
813
            }
814
        } else {
815 1
            $unixMode = 0100644;
816
817 1
            if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) {
818 1
                $compressionMethod = ZipCompressionMethod::DEFLATED;
819
            }
820
        }
821
822 5
        $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
823 5
        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
824 5
        $zipEntry->setUnixMode($unixMode);
825 5
        $zipEntry->setCompressionMethod($compressionMethod);
826 4
        $zipEntry->setTime(time());
827 4
        $zipEntry->setData(new ZipNewData($zipEntry, $stream));
828
829 4
        $this->addZipEntry($zipEntry);
830
831 4
        return $this;
832
    }
833
834
    /**
835
     * Add an empty directory in the zip archive.
836
     *
837
     * @param string $dirName
838
     *
839
     * @throws ZipException
840
     *
841
     * @return ZipFile
842
     */
843 21
    public function addEmptyDir($dirName)
844
    {
845 21
        if ($dirName === null) {
0 ignored issues
show
introduced by
The condition $dirName === null is always false.
Loading history...
846 1
            throw new InvalidArgumentException('Dir name is null');
847
        }
848 20
        $dirName = ltrim((string) $dirName, '\\/');
849
850 20
        if ($dirName === '') {
851 1
            throw new InvalidArgumentException('Empty dir name');
852
        }
853 19
        $dirName = rtrim($dirName, '\\/') . '/';
854
855 19
        $zipEntry = new ZipEntry($dirName);
856 19
        $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
857 19
        $zipEntry->setUncompressedSize(0);
858 19
        $zipEntry->setCompressedSize(0);
859 19
        $zipEntry->setCrc(0);
860 19
        $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
861 19
        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
862 19
        $zipEntry->setUnixMode(040755);
863 19
        $zipEntry->setTime(time());
864
865 19
        $this->addZipEntry($zipEntry);
866
867 19
        return $this;
868
    }
869
870
    /**
871
     * Add directory not recursively to the zip archive.
872
     *
873
     * @param string   $inputDir          Input directory
874
     * @param string   $localPath         add files to this directory, or the root
875
     * @param int|null $compressionMethod Compression method.
876
     *
877
     *                                    Use {@see ZipCompressionMethod::STORED}, {@see
878
     *     ZipCompressionMethod::DEFLATED} or
879
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
880
     *
881
     * @throws ZipException
882
     *
883
     * @return ZipFile
884
     */
885 10
    public function addDir($inputDir, $localPath = '/', $compressionMethod = null)
886
    {
887 10
        if ($inputDir === null) {
0 ignored issues
show
introduced by
The condition $inputDir === null is always false.
Loading history...
888 1
            throw new InvalidArgumentException('Input dir is null');
889
        }
890 9
        $inputDir = (string) $inputDir;
891
892 9
        if ($inputDir === '') {
893 1
            throw new InvalidArgumentException('The input directory is not specified');
894
        }
895
896 8
        if (!is_dir($inputDir)) {
897 1
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
898
        }
899 7
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
900
901 7
        $directoryIterator = new \DirectoryIterator($inputDir);
902
903 7
        return $this->addFilesFromIterator($directoryIterator, $localPath, $compressionMethod);
904
    }
905
906
    /**
907
     * Add recursive directory to the zip archive.
908
     *
909
     * @param string   $inputDir          Input directory
910
     * @param string   $localPath         add files to this directory, or the root
911
     * @param int|null $compressionMethod Compression method.
912
     *                                    Use {@see ZipCompressionMethod::STORED}, {@see
913
     *                                    ZipCompressionMethod::DEFLATED} or
914
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
915
     *
916
     * @throws ZipException
917
     *
918
     * @return ZipFile
919
     *
920
     * @see ZipCompressionMethod::STORED
921
     * @see ZipCompressionMethod::DEFLATED
922
     * @see ZipCompressionMethod::BZIP2
923
     */
924 11
    public function addDirRecursive($inputDir, $localPath = '/', $compressionMethod = null)
925
    {
926 11
        if ($inputDir === null) {
0 ignored issues
show
introduced by
The condition $inputDir === null is always false.
Loading history...
927 1
            throw new InvalidArgumentException('Input dir is null');
928
        }
929 10
        $inputDir = (string) $inputDir;
930
931 10
        if ($inputDir === '') {
932 1
            throw new InvalidArgumentException('The input directory is not specified');
933
        }
934
935 9
        if (!is_dir($inputDir)) {
936 1
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
937
        }
938 8
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
939
940 8
        $directoryIterator = new \RecursiveDirectoryIterator($inputDir);
941
942 8
        return $this->addFilesFromIterator($directoryIterator, $localPath, $compressionMethod);
943
    }
944
945
    /**
946
     * Add directories from directory iterator.
947
     *
948
     * @param \Iterator $iterator          directory iterator
949
     * @param string    $localPath         add files to this directory, or the root
950
     * @param int|null  $compressionMethod Compression method.
951
     *                                     Use {@see ZipCompressionMethod::STORED}, {@see
952
     *                                     ZipCompressionMethod::DEFLATED} or
953
     *                                     {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
954
     *
955
     * @throws ZipException
956
     *
957
     * @return ZipFile
958
     *
959
     * @see ZipCompressionMethod::STORED
960
     * @see ZipCompressionMethod::DEFLATED
961
     * @see ZipCompressionMethod::BZIP2
962
     */
963 21
    public function addFilesFromIterator(
964
        \Iterator $iterator,
965
        $localPath = '/',
966
        $compressionMethod = null
967
    ) {
968 21
        $localPath = (string) $localPath;
969
970 21
        if ($localPath !== '') {
971 20
            $localPath = trim($localPath, '\\/');
972
        } else {
973 1
            $localPath = '';
974
        }
975
976 21
        $iterator = $iterator instanceof \RecursiveIterator ?
977 11
            new \RecursiveIteratorIterator($iterator) :
978 21
            new \IteratorIterator($iterator);
979
        /**
980
         * @var string[] $files
981
         * @var string   $path
982
         */
983 21
        $files = [];
984
985 21
        foreach ($iterator as $file) {
986 21
            if ($file instanceof \SplFileInfo) {
987 21
                if ($file->getBasename() === '..') {
988 19
                    continue;
989
                }
990
991 21
                if ($file->getBasename() === '.') {
992 21
                    $files[] = \dirname($file->getPathname());
993
                } else {
994 21
                    $files[] = $file->getPathname();
995
                }
996
            }
997
        }
998
999 21
        if (empty($files)) {
1000
            return $this;
1001
        }
1002
1003 21
        natcasesort($files);
1004 21
        $path = array_shift($files);
1005
1006 21
        $this->doAddFiles($path, $files, $localPath, $compressionMethod);
1007
1008 21
        return $this;
1009
    }
1010
1011
    /**
1012
     * Add files from glob pattern.
1013
     *
1014
     * @param string   $inputDir          Input directory
1015
     * @param string   $globPattern       glob pattern
1016
     * @param string   $localPath         add files to this directory, or the root
1017
     * @param int|null $compressionMethod Compression method.
1018
     *                                    Use {@see ZipCompressionMethod::STORED},
1019
     *                                    {@see ZipCompressionMethod::DEFLATED} or
1020
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
1021
     *
1022
     * @throws ZipException
1023
     *
1024
     * @return ZipFile
1025
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
1026
     */
1027 6
    public function addFilesFromGlob($inputDir, $globPattern, $localPath = '/', $compressionMethod = null)
1028
    {
1029 6
        return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod);
1030
    }
1031
1032
    /**
1033
     * Add files from glob pattern.
1034
     *
1035
     * @param string   $inputDir          Input directory
1036
     * @param string   $globPattern       glob pattern
1037
     * @param string   $localPath         add files to this directory, or the root
1038
     * @param bool     $recursive         recursive search
1039
     * @param int|null $compressionMethod Compression method.
1040
     *                                    Use {@see ZipCompressionMethod::STORED},
1041
     *                                    {@see ZipCompressionMethod::DEFLATED} or
1042
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
1043
     *
1044
     * @throws ZipException
1045
     *
1046
     * @return ZipFile
1047
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
1048
     */
1049 14
    private function addGlob(
1050
        $inputDir,
1051
        $globPattern,
1052
        $localPath = '/',
1053
        $recursive = true,
1054
        $compressionMethod = null
1055
    ) {
1056 14
        if ($inputDir === null) {
0 ignored issues
show
introduced by
The condition $inputDir === null is always false.
Loading history...
1057 2
            throw new InvalidArgumentException('Input dir is null');
1058
        }
1059 12
        $inputDir = (string) $inputDir;
1060
1061 12
        if ($inputDir === '') {
1062 2
            throw new InvalidArgumentException('The input directory is not specified');
1063
        }
1064
1065 10
        if (!is_dir($inputDir)) {
1066 3
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
1067
        }
1068 7
        $globPattern = (string) $globPattern;
1069
1070 7
        if (empty($globPattern)) {
1071 4
            throw new InvalidArgumentException('The glob pattern is not specified');
1072
        }
1073
1074 3
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
1075 3
        $globPattern = $inputDir . $globPattern;
1076
1077 3
        $filesFound = FilesUtil::globFileSearch($globPattern, \GLOB_BRACE, $recursive);
1078
1079 3
        if ($filesFound === false || empty($filesFound)) {
1080
            return $this;
1081
        }
1082
1083 3
        $this->doAddFiles($inputDir, $filesFound, $localPath, $compressionMethod);
1084
1085 3
        return $this;
1086
    }
1087
1088
    /**
1089
     * Add files recursively from glob pattern.
1090
     *
1091
     * @param string   $inputDir          Input directory
1092
     * @param string   $globPattern       glob pattern
1093
     * @param string   $localPath         add files to this directory, or the root
1094
     * @param int|null $compressionMethod Compression method.
1095
     *                                    Use {@see ZipCompressionMethod::STORED},
1096
     *                                    {@see ZipCompressionMethod::DEFLATED} or
1097
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
1098
     *
1099
     * @throws ZipException
1100
     *
1101
     * @return ZipFile
1102
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
1103
     */
1104 8
    public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null)
1105
    {
1106 8
        return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod);
1107
    }
1108
1109
    /**
1110
     * Add files from regex pattern.
1111
     *
1112
     * @param string   $inputDir          search files in this directory
1113
     * @param string   $regexPattern      regex pattern
1114
     * @param string   $localPath         add files to this directory, or the root
1115
     * @param int|null $compressionMethod Compression method.
1116
     *                                    Use {@see ZipCompressionMethod::STORED},
1117
     *                                    {@see ZipCompressionMethod::DEFLATED} or
1118
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
1119
     *
1120
     * @throws ZipException
1121
     *
1122
     * @return ZipFile
1123
     *
1124
     * @internal param bool $recursive Recursive search
1125
     */
1126 6
    public function addFilesFromRegex($inputDir, $regexPattern, $localPath = '/', $compressionMethod = null)
1127
    {
1128 6
        return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod);
1129
    }
1130
1131
    /**
1132
     * Add files from regex pattern.
1133
     *
1134
     * @param string   $inputDir          search files in this directory
1135
     * @param string   $regexPattern      regex pattern
1136
     * @param string   $localPath         add files to this directory, or the root
1137
     * @param bool     $recursive         recursive search
1138
     * @param int|null $compressionMethod Compression method.
1139
     *                                    Use {@see ZipCompressionMethod::STORED},
1140
     *                                    {@see ZipCompressionMethod::DEFLATED} or
1141
     *                                    {@see ZipCompressionMethod::BZIP2}.
1142
     *                                    If null, then auto choosing method.
1143
     *
1144
     * @throws ZipException
1145
     *
1146
     * @return ZipFile
1147
     */
1148 12
    private function addRegex(
1149
        $inputDir,
1150
        $regexPattern,
1151
        $localPath = '/',
1152
        $recursive = true,
1153
        $compressionMethod = null
1154
    ) {
1155 12
        $regexPattern = (string) $regexPattern;
1156
1157 12
        if (empty($regexPattern)) {
1158 4
            throw new InvalidArgumentException('The regex pattern is not specified');
1159
        }
1160 8
        $inputDir = (string) $inputDir;
1161
1162 8
        if ($inputDir === '') {
1163 4
            throw new InvalidArgumentException('The input directory is not specified');
1164
        }
1165
1166 4
        if (!is_dir($inputDir)) {
1167 1
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
1168
        }
1169 3
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
1170
1171 3
        $files = FilesUtil::regexFileSearch($inputDir, $regexPattern, $recursive);
1172
1173 3
        if (empty($files)) {
1174
            return $this;
1175
        }
1176
1177 3
        $this->doAddFiles($inputDir, $files, $localPath, $compressionMethod);
1178
1179 3
        return $this;
1180
    }
1181
1182
    /**
1183
     * @param string   $fileSystemDir
1184
     * @param array    $files
1185
     * @param string   $zipPath
1186
     * @param int|null $compressionMethod
1187
     *
1188
     * @throws ZipException
1189
     */
1190 27
    private function doAddFiles($fileSystemDir, array $files, $zipPath, $compressionMethod = null)
1191
    {
1192 27
        $fileSystemDir = rtrim($fileSystemDir, '/\\') . \DIRECTORY_SEPARATOR;
1193
1194 27
        if (!empty($zipPath) && \is_string($zipPath)) {
1195 13
            $zipPath = trim($zipPath, '\\/') . '/';
1196
        } else {
1197 14
            $zipPath = '/';
1198
        }
1199
1200
        /**
1201
         * @var string $file
1202
         */
1203 27
        foreach ($files as $file) {
1204 27
            $filename = str_replace($fileSystemDir, $zipPath, $file);
1205 27
            $filename = ltrim($filename, '\\/');
1206
1207 27
            if (is_dir($file) && FilesUtil::isEmptyDir($file)) {
1208 15
                $this->addEmptyDir($filename);
1209 27
            } elseif (is_file($file)) {
1210 27
                $this->addFile($file, $filename, $compressionMethod);
1211
            }
1212
        }
1213 27
    }
1214
1215
    /**
1216
     * Add files recursively from regex pattern.
1217
     *
1218
     * @param string   $inputDir          search files in this directory
1219
     * @param string   $regexPattern      regex pattern
1220
     * @param string   $localPath         add files to this directory, or the root
1221
     * @param int|null $compressionMethod Compression method.
1222
     *                                    Use {@see ZipCompressionMethod::STORED},
1223
     *                                    {@see ZipCompressionMethod::DEFLATED} or
1224
     *                                    {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
1225
     *
1226
     * @throws ZipException
1227
     *
1228
     * @return ZipFile
1229
     *
1230
     * @internal param bool $recursive Recursive search
1231
     */
1232 6
    public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = '/', $compressionMethod = null)
1233
    {
1234 6
        return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod);
1235
    }
1236
1237
    /**
1238
     * Add array data to archive.
1239
     * Keys is local names.
1240
     * Values is contents.
1241
     *
1242
     * @param array $mapData associative array for added to zip
1243
     */
1244 1
    public function addAll(array $mapData)
1245
    {
1246 1
        foreach ($mapData as $localName => $content) {
1247 1
            $this[$localName] = $content;
1248
        }
1249 1
    }
1250
1251
    /**
1252
     * Rename the entry.
1253
     *
1254
     * @param string $oldName old entry name
1255
     * @param string $newName new entry name
1256
     *
1257
     * @throws ZipException
1258
     *
1259
     * @return ZipFile
1260
     */
1261 7
    public function rename($oldName, $newName)
1262
    {
1263 7
        if ($oldName === null || $newName === null) {
0 ignored issues
show
introduced by
The condition $newName === null is always false.
Loading history...
1264 2
            throw new InvalidArgumentException('name is null');
1265
        }
1266 5
        $oldName = ltrim((string) $oldName, '\\/');
1267 5
        $newName = ltrim((string) $newName, '\\/');
1268
1269 5
        if ($oldName !== $newName) {
1270 5
            $this->zipContainer->renameEntry($oldName, $newName);
1271
        }
1272
1273 3
        return $this;
1274
    }
1275
1276
    /**
1277
     * Delete entry by name.
1278
     *
1279
     * @param string $entryName zip Entry name
1280
     *
1281
     * @throws ZipEntryNotFoundException if entry not found
1282
     *
1283
     * @return ZipFile
1284
     */
1285 5
    public function deleteFromName($entryName)
1286
    {
1287 5
        $entryName = ltrim((string) $entryName, '\\/');
1288
1289 5
        if (!$this->zipContainer->deleteEntry($entryName)) {
1290 1
            throw new ZipEntryNotFoundException($entryName);
1291
        }
1292
1293 4
        return $this;
1294
    }
1295
1296
    /**
1297
     * Delete entries by glob pattern.
1298
     *
1299
     * @param string $globPattern Glob pattern
1300
     *
1301
     * @return ZipFile
1302
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
1303
     */
1304 3
    public function deleteFromGlob($globPattern)
1305
    {
1306 3
        if ($globPattern === null || !\is_string($globPattern) || empty($globPattern)) {
0 ignored issues
show
introduced by
The condition is_string($globPattern) is always true.
Loading history...
1307 2
            throw new InvalidArgumentException('The glob pattern is not specified');
1308
        }
1309 1
        $globPattern = '~' . FilesUtil::convertGlobToRegEx($globPattern) . '~si';
1310 1
        $this->deleteFromRegex($globPattern);
1311
1312 1
        return $this;
1313
    }
1314
1315
    /**
1316
     * Delete entries by regex pattern.
1317
     *
1318
     * @param string $regexPattern Regex pattern
1319
     *
1320
     * @return ZipFile
1321
     */
1322 6
    public function deleteFromRegex($regexPattern)
1323
    {
1324 6
        if ($regexPattern === null || !\is_string($regexPattern) || empty($regexPattern)) {
0 ignored issues
show
introduced by
The condition is_string($regexPattern) is always true.
Loading history...
1325 2
            throw new InvalidArgumentException('The regex pattern is not specified');
1326
        }
1327 4
        $this->matcher()->match($regexPattern)->delete();
1328
1329 4
        return $this;
1330
    }
1331
1332
    /**
1333
     * Delete all entries.
1334
     *
1335
     * @return ZipFile
1336
     */
1337 1
    public function deleteAll()
1338
    {
1339 1
        $this->zipContainer->deleteAll();
1340
1341 1
        return $this;
1342
    }
1343
1344
    /**
1345
     * Set compression level for new entries.
1346
     *
1347
     * @param int $compressionLevel
1348
     *
1349
     * @return ZipFile
1350
     *
1351
     * @see ZipCompressionLevel::NORMAL
1352
     * @see ZipCompressionLevel::SUPER_FAST
1353
     * @see ZipCompressionLevel::FAST
1354
     * @see ZipCompressionLevel::MAXIMUM
1355
     */
1356 8
    public function setCompressionLevel($compressionLevel = ZipCompressionLevel::NORMAL)
1357
    {
1358 8
        $compressionLevel = (int) $compressionLevel;
1359
1360 8
        foreach ($this->zipContainer->getEntries() as $entry) {
1361 7
            $entry->setCompressionLevel($compressionLevel);
1362
        }
1363
1364 3
        return $this;
1365
    }
1366
1367
    /**
1368
     * @param string $entryName
1369
     * @param int    $compressionLevel
1370
     *
1371
     * @throws ZipException
1372
     *
1373
     * @return ZipFile
1374
     *
1375
     * @see ZipCompressionLevel::NORMAL
1376
     * @see ZipCompressionLevel::SUPER_FAST
1377
     * @see ZipCompressionLevel::FAST
1378
     * @see ZipCompressionLevel::MAXIMUM
1379
     */
1380 5
    public function setCompressionLevelEntry($entryName, $compressionLevel)
1381
    {
1382 5
        $compressionLevel = (int) $compressionLevel;
1383 5
        $this->getEntry($entryName)->setCompressionLevel($compressionLevel);
1384
1385 4
        return $this;
1386
    }
1387
1388
    /**
1389
     * @param string $entryName
1390
     * @param int    $compressionMethod Compression method.
1391
     *                                  Use {@see ZipCompressionMethod::STORED}, {@see ZipCompressionMethod::DEFLATED}
1392
     *                                  or
1393
     *                                  {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method.
1394
     *
1395
     * @throws ZipException
1396
     *
1397
     * @return ZipFile
1398
     *
1399
     * @see ZipCompressionMethod::STORED
1400
     * @see ZipCompressionMethod::DEFLATED
1401
     * @see ZipCompressionMethod::BZIP2
1402
     */
1403 3
    public function setCompressionMethodEntry($entryName, $compressionMethod)
1404
    {
1405 3
        $this->zipContainer
1406 3
            ->getEntry($entryName)
1407 3
            ->setCompressionMethod($compressionMethod)
1408
        ;
1409
1410 2
        return $this;
1411
    }
1412
1413
    /**
1414
     * zipalign is optimization to Android application (APK) files.
1415
     *
1416
     * @param int|null $align
1417
     *
1418
     * @return ZipFile
1419
     *
1420
     * @see https://developer.android.com/studio/command-line/zipalign.html
1421
     */
1422 4
    public function setZipAlign($align = null)
1423
    {
1424 4
        $this->zipContainer->setZipAlign($align);
1425
1426 4
        return $this;
1427
    }
1428
1429
    /**
1430
     * Set password to all input encrypted entries.
1431
     *
1432
     * @param string $password Password
1433
     *
1434
     * @return ZipFile
1435
     */
1436 9
    public function setReadPassword($password)
1437
    {
1438 9
        $this->zipContainer->setReadPassword($password);
1439
1440 9
        return $this;
1441
    }
1442
1443
    /**
1444
     * Set password to concrete input entry.
1445
     *
1446
     * @param string $entryName
1447
     * @param string $password  Password
1448
     *
1449
     * @throws ZipException
1450
     *
1451
     * @return ZipFile
1452
     */
1453 2
    public function setReadPasswordEntry($entryName, $password)
1454
    {
1455 2
        $this->zipContainer->setReadPasswordEntry($entryName, $password);
1456
1457 2
        return $this;
1458
    }
1459
1460
    /**
1461
     * Sets a new password for all files in the archive.
1462
     *
1463
     * @param string   $password         Password
1464
     * @param int|null $encryptionMethod Encryption method
1465
     *
1466
     * @return ZipFile
1467
     */
1468 11
    public function setPassword($password, $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256)
1469
    {
1470 11
        $this->zipContainer->setWritePassword($password);
1471
1472 11
        if ($encryptionMethod !== null) {
1473 11
            $this->zipContainer->setEncryptionMethod($encryptionMethod);
1474
        }
1475
1476 10
        return $this;
1477
    }
1478
1479
    /**
1480
     * Sets a new password of an entry defined by its name.
1481
     *
1482
     * @param string   $entryName
1483
     * @param string   $password
1484
     * @param int|null $encryptionMethod
1485
     *
1486
     * @throws ZipException
1487
     *
1488
     * @return ZipFile
1489
     */
1490 5
    public function setPasswordEntry($entryName, $password, $encryptionMethod = null)
1491
    {
1492 5
        $this->getEntry($entryName)->setPassword($password, $encryptionMethod);
1493
1494 4
        return $this;
1495
    }
1496
1497
    /**
1498
     * Disable encryption for all entries that are already in the archive.
1499
     *
1500
     * @return ZipFile
1501
     */
1502 2
    public function disableEncryption()
1503
    {
1504 2
        $this->zipContainer->removePassword();
1505
1506 2
        return $this;
1507
    }
1508
1509
    /**
1510
     * Disable encryption of an entry defined by its name.
1511
     *
1512
     * @param string $entryName
1513
     *
1514
     * @return ZipFile
1515
     */
1516 1
    public function disableEncryptionEntry($entryName)
1517
    {
1518 1
        $this->zipContainer->removePasswordEntry($entryName);
1519
1520 1
        return $this;
1521
    }
1522
1523
    /**
1524
     * Undo all changes done in the archive.
1525
     *
1526
     * @return ZipFile
1527
     */
1528 1
    public function unchangeAll()
1529
    {
1530 1
        $this->zipContainer->unchangeAll();
1531
1532 1
        return $this;
1533
    }
1534
1535
    /**
1536
     * Undo change archive comment.
1537
     *
1538
     * @return ZipFile
1539
     */
1540 1
    public function unchangeArchiveComment()
1541
    {
1542 1
        $this->zipContainer->unchangeArchiveComment();
1543
1544 1
        return $this;
1545
    }
1546
1547
    /**
1548
     * Revert all changes done to an entry with the given name.
1549
     *
1550
     * @param string|ZipEntry $entry Entry name or ZipEntry
1551
     *
1552
     * @return ZipFile
1553
     */
1554 1
    public function unchangeEntry($entry)
1555
    {
1556 1
        $this->zipContainer->unchangeEntry($entry);
1557
1558 1
        return $this;
1559
    }
1560
1561
    /**
1562
     * Save as file.
1563
     *
1564
     * @param string $filename Output filename
1565
     *
1566
     * @throws ZipException
1567
     *
1568
     * @return ZipFile
1569
     */
1570 76
    public function saveAsFile($filename)
1571
    {
1572 76
        $filename = (string) $filename;
1573
1574 76
        $tempFilename = $filename . '.temp' . uniqid('', true);
1575
1576 76
        if (!($handle = @fopen($tempFilename, 'w+b'))) {
1577 1
            throw new InvalidArgumentException('File ' . $tempFilename . ' can not open from write.');
1578
        }
1579 75
        $this->saveAsStream($handle);
1580
1581 75
        if (!@rename($tempFilename, $filename)) {
1582
            if (is_file($tempFilename)) {
1583
                unlink($tempFilename);
1584
            }
1585
1586
            throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename);
1587
        }
1588
1589 75
        return $this;
1590
    }
1591
1592
    /**
1593
     * Save as stream.
1594
     *
1595
     * @param resource $handle Output stream resource
1596
     *
1597
     * @throws ZipException
1598
     *
1599
     * @return ZipFile
1600
     */
1601 76
    public function saveAsStream($handle)
1602
    {
1603 76
        if (!\is_resource($handle)) {
1604 1
            throw new InvalidArgumentException('handle is not resource');
1605
        }
1606 75
        ftruncate($handle, 0);
1607 75
        $this->writeZipToStream($handle);
1608 75
        fclose($handle);
1609
1610 75
        return $this;
1611
    }
1612
1613
    /**
1614
     * Output .ZIP archive as attachment.
1615
     * Die after output.
1616
     *
1617
     * @param string      $outputFilename Output filename
1618
     * @param string|null $mimeType       Mime-Type
1619
     * @param bool        $attachment     Http Header 'Content-Disposition' if true then attachment otherwise inline
1620
     *
1621
     * @throws ZipException
1622
     */
1623 3
    public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true)
1624
    {
1625 3
        $outputFilename = (string) $outputFilename;
1626
1627 3
        if ($mimeType === null) {
1628 2
            $mimeType = $this->getMimeTypeByFilename($outputFilename);
1629
        }
1630
1631 3
        if (!($handle = fopen('php://temp', 'w+b'))) {
1632
            throw new InvalidArgumentException('php://temp cannot open for write.');
1633
        }
1634 3
        $this->writeZipToStream($handle);
1635 3
        $this->close();
1636
1637 3
        $size = fstat($handle)['size'];
1638
1639 3
        $headerContentDisposition = 'Content-Disposition: ' . ($attachment ? 'attachment' : 'inline');
1640
1641 3
        if (!empty($outputFilename)) {
1642 3
            $headerContentDisposition .= '; filename="' . basename($outputFilename) . '"';
1643
        }
1644
1645 3
        header($headerContentDisposition);
1646 3
        header('Content-Type: ' . $mimeType);
1647 3
        header('Content-Length: ' . $size);
1648
1649 3
        rewind($handle);
1650
1651
        try {
1652 3
            echo stream_get_contents($handle, -1, 0);
1653 3
        } finally {
1654 3
            fclose($handle);
1655
        }
1656 3
    }
1657
1658
    /**
1659
     * @param string $outputFilename
1660
     *
1661
     * @return string
1662
     */
1663 3
    protected function getMimeTypeByFilename($outputFilename)
1664
    {
1665 3
        $outputFilename = (string) $outputFilename;
1666 3
        $ext = strtolower(pathinfo($outputFilename, \PATHINFO_EXTENSION));
1667
1668 3
        if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) {
1669 3
            return self::$defaultMimeTypes[$ext];
1670
        }
1671
1672
        return self::$defaultMimeTypes['zip'];
1673
    }
1674
1675
    /**
1676
     * Output .ZIP archive as PSR-7 Response.
1677
     *
1678
     * @param ResponseInterface $response       Instance PSR-7 Response
1679
     * @param string            $outputFilename Output filename
1680
     * @param string|null       $mimeType       Mime-Type
1681
     * @param bool              $attachment     Http Header 'Content-Disposition' if true then attachment otherwise inline
1682
     *
1683
     * @throws ZipException
1684
     *
1685
     * @return ResponseInterface
1686
     */
1687 1
    public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true)
1688
    {
1689 1
        $outputFilename = (string) $outputFilename;
1690
1691 1
        if ($mimeType === null) {
1692 1
            $mimeType = $this->getMimeTypeByFilename($outputFilename);
1693
        }
1694
1695 1
        if (!($handle = fopen('php://temp', 'w+b'))) {
1696
            throw new InvalidArgumentException('php://temp cannot open for write.');
1697
        }
1698 1
        $this->writeZipToStream($handle);
1699 1
        $this->close();
1700 1
        rewind($handle);
1701
1702 1
        $contentDispositionValue = ($attachment ? 'attachment' : 'inline');
1703
1704 1
        if (!empty($outputFilename)) {
1705 1
            $contentDispositionValue .= '; filename="' . basename($outputFilename) . '"';
1706
        }
1707
1708 1
        $stream = new ResponseStream($handle);
1709 1
        $size = $stream->getSize();
1710
1711 1
        if ($size !== null) {
1712
            /** @noinspection CallableParameterUseCaseInTypeContextInspection */
1713 1
            $response = $response->withHeader('Content-Length', (string) $size);
1714
        }
1715
1716
        return $response
1717 1
            ->withHeader('Content-Type', $mimeType)
1718 1
            ->withHeader('Content-Disposition', $contentDispositionValue)
1719 1
            ->withBody($stream)
1720
        ;
1721
    }
1722
1723
    /**
1724
     * @param resource $handle
1725
     *
1726
     * @throws ZipException
1727
     */
1728 80
    protected function writeZipToStream($handle)
1729
    {
1730 80
        $this->onBeforeSave();
1731
1732 80
        $this->createZipWriter()->write($handle);
1733 80
    }
1734
1735
    /**
1736
     * Returns the zip archive as a string.
1737
     *
1738
     * @throws ZipException
1739
     *
1740
     * @return string
1741
     */
1742 1
    public function outputAsString()
1743
    {
1744 1
        if (!($handle = fopen('php://temp', 'w+b'))) {
1745
            throw new InvalidArgumentException('php://temp cannot open for write.');
1746
        }
1747 1
        $this->writeZipToStream($handle);
1748 1
        rewind($handle);
1749
1750
        try {
1751 1
            return stream_get_contents($handle);
1752
        } finally {
1753 1
            fclose($handle);
1754
        }
1755
    }
1756
1757
    /**
1758
     * Event before save or output.
1759
     */
1760 80
    protected function onBeforeSave()
1761
    {
1762 80
    }
1763
1764
    /**
1765
     * Close zip archive and release input stream.
1766
     */
1767 178
    public function close()
1768
    {
1769 178
        if ($this->reader !== null) {
1770 90
            $this->reader->close();
1771 90
            $this->reader = null;
1772 90
            $this->zipContainer = new ZipContainer();
1773
        }
1774 178
    }
1775
1776
    /**
1777
     * Save and reopen zip archive.
1778
     *
1779
     * @throws ZipException
1780
     *
1781
     * @return ZipFile
1782
     */
1783 6
    public function rewrite()
1784
    {
1785 6
        if ($this->reader === null) {
1786 1
            throw new ZipException('input stream is null');
1787
        }
1788
1789 5
        $meta = $this->reader->getStreamMetaData();
1790
1791 5
        if ($meta['wrapper_type'] === 'plainfile' && isset($meta['uri'])) {
1792 4
            $this->saveAsFile($meta['uri']);
1793 4
            $this->close();
1794
1795 4
            if (!($handle = @fopen($meta['uri'], 'rb'))) {
1796 4
                throw new ZipException("File {$meta['uri']} can't open.");
1797
            }
1798
        } else {
1799 1
            $handle = @fopen('php://temp', 'r+b');
1800
1801 1
            if (!$handle) {
0 ignored issues
show
introduced by
$handle is of type false|resource, thus it always evaluated to false.
Loading history...
1802
                throw new ZipException('php://temp cannot open for write.');
1803
            }
1804 1
            $this->writeZipToStream($handle);
1805 1
            $this->close();
1806
        }
1807
1808 5
        return $this->openFromStream($handle);
1809
    }
1810
1811
    /**
1812
     * Release all resources.
1813
     */
1814 174
    public function __destruct()
1815
    {
1816 174
        $this->close();
1817 174
    }
1818
1819
    /**
1820
     * Offset to set.
1821
     *
1822
     * @see http://php.net/manual/en/arrayaccess.offsetset.php
1823
     *
1824
     * @param string                                          $entryName the offset to assign the value to
1825
     * @param string|\DirectoryIterator|\SplFileInfo|resource $contents  the value to set
1826
     *
1827
     * @throws ZipException
1828
     *
1829
     * @see ZipFile::addFromString
1830
     * @see ZipFile::addEmptyDir
1831
     * @see ZipFile::addFile
1832
     * @see ZipFile::addFilesFromIterator
1833
     */
1834 35
    public function offsetSet($entryName, $contents)
1835
    {
1836 35
        if ($entryName === null) {
0 ignored issues
show
introduced by
The condition $entryName === null is always false.
Loading history...
1837 1
            throw new InvalidArgumentException('Key must not be null, but must contain the name of the zip entry.');
1838
        }
1839 34
        $entryName = ltrim((string) $entryName, '\\/');
1840
1841 34
        if ($entryName === '') {
1842 1
            throw new InvalidArgumentException('Key is empty, but must contain the name of the zip entry.');
1843
        }
1844
1845 33
        if ($contents instanceof \DirectoryIterator) {
1846 1
            $this->addFilesFromIterator($contents, $entryName);
1847 32
        } elseif ($contents instanceof \SplFileInfo) {
1848 1
            $this->addSplFile($contents, $entryName);
1849 32
        } elseif (StringUtil::endsWith($entryName, '/')) {
1850 3
            $this->addEmptyDir($entryName);
1851 32
        } elseif (\is_resource($contents)) {
1852 1
            $this->addFromStream($contents, $entryName);
1853
        } else {
1854 31
            $this->addFromString($entryName, (string) $contents);
1855
        }
1856 33
    }
1857
1858
    /**
1859
     * Offset to unset.
1860
     *
1861
     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
1862
     *
1863
     * @param string $entryName the offset to unset
1864
     *
1865
     * @throws ZipEntryNotFoundException
1866
     */
1867 2
    public function offsetUnset($entryName)
1868
    {
1869 2
        $this->deleteFromName($entryName);
1870 2
    }
1871
1872
    /**
1873
     * Return the current element.
1874
     *
1875
     * @see http://php.net/manual/en/iterator.current.php
1876
     *
1877
     * @throws ZipException
1878
     *
1879
     * @return mixed can return any type
1880
     *
1881
     * @since 5.0.0
1882
     */
1883 5
    public function current()
1884
    {
1885 5
        return $this->offsetGet($this->key());
1886
    }
1887
1888
    /**
1889
     * Offset to retrieve.
1890
     *
1891
     * @see http://php.net/manual/en/arrayaccess.offsetget.php
1892
     *
1893
     * @param string $entryName the offset to retrieve
1894
     *
1895
     * @throws ZipException
1896
     *
1897
     * @return string|null
1898
     */
1899 43
    public function offsetGet($entryName)
1900
    {
1901 43
        return $this->getEntryContents($entryName);
1902
    }
1903
1904
    /**
1905
     * Return the key of the current element.
1906
     *
1907
     * @see http://php.net/manual/en/iterator.key.php
1908
     *
1909
     * @return mixed scalar on success, or null on failure
1910
     *
1911
     * @since 5.0.0
1912
     */
1913 5
    public function key()
1914
    {
1915 5
        return key($this->zipContainer->getEntries());
1916
    }
1917
1918
    /**
1919
     * Move forward to next element.
1920
     *
1921
     * @see http://php.net/manual/en/iterator.next.php
1922
     * @since 5.0.0
1923
     */
1924 5
    public function next()
1925
    {
1926 5
        next($this->zipContainer->getEntries());
1927 5
    }
1928
1929
    /**
1930
     * Checks if current position is valid.
1931
     *
1932
     * @see http://php.net/manual/en/iterator.valid.php
1933
     *
1934
     * @return bool The return value will be casted to boolean and then evaluated.
1935
     *              Returns true on success or false on failure.
1936
     *
1937
     * @since 5.0.0
1938
     */
1939 5
    public function valid()
1940
    {
1941 5
        return $this->offsetExists($this->key());
1942
    }
1943
1944
    /**
1945
     * Whether a offset exists.
1946
     *
1947
     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
1948
     *
1949
     * @param string $entryName an offset to check for
1950
     *
1951
     * @return bool true on success or false on failure.
1952
     *              The return value will be casted to boolean if non-boolean was returned.
1953
     */
1954 42
    public function offsetExists($entryName)
1955
    {
1956 42
        return $this->hasEntry($entryName);
1957
    }
1958
1959
    /**
1960
     * Rewind the Iterator to the first element.
1961
     *
1962
     * @see http://php.net/manual/en/iterator.rewind.php
1963
     * @since 5.0.0
1964
     */
1965 5
    public function rewind()
1966
    {
1967 5
        reset($this->zipContainer->getEntries());
1968 5
    }
1969
}
1970