Passed
Push — master ( 391a55...501b52 )
by Alexey
08:40 queued 11s
created

src/ZipFile.php (5 issues)

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