Issues (54)

src/ZipFile.php (12 issues)

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