Completed
Push — master ( 074443...b3b676 )
by Alexey
08:58 queued 11s
created

ZipFile::createZipContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
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
0 ignored issues
show
Deprecated Code introduced by
The interface PhpZip\ZipFileInterface has been deprecated: will be removed in version 4.0. Use the {@see ZipFile} class. ( Ignorable by Annotation )

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

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

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

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

Loading history...
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 175
     * ZipFile constructor.
70
     */
71 175
    public function __construct()
72 175
    {
73
        $this->zipContainer = $this->createZipContainer(null);
74
    }
75
76
    /**
77
     * @param resource $inputStream
78
     * @param array    $options
79
     *
80 98
     * @return ZipReader
81
     */
82 98
    protected function createZipReader($inputStream, array $options = [])
83
    {
84
        return new ZipReader($inputStream, $options);
85
    }
86
87
    /**
88 81
     * @return ZipWriter
89
     */
90 81
    protected function createZipWriter()
91
    {
92
        return new ZipWriter($this->zipContainer);
93
    }
94
95
    /**
96
     * @param ImmutableZipContainer|null $sourceContainer
97
     *
98
     * @return ZipContainer
99
     */
100
    protected function createZipContainer(ImmutableZipContainer $sourceContainer = null)
101
    {
102
        return new ZipContainer($sourceContainer);
103 80
    }
104
105 80
    /**
106 1
     * Open zip archive from file.
107
     *
108
     * @param string $filename
109 79
     * @param array  $options
110 1
     *
111
     * @throws ZipException if can't open file
112
     *
113 78
     * @return ZipFile
114
     */
115
    public function openFile($filename, array $options = [])
116
    {
117
        if (!file_exists($filename)) {
118
            throw new ZipException("File {$filename} does not exist.");
119
        }
120
121
        if (!($handle = @fopen($filename, 'rb'))) {
122
            throw new ZipException("File {$filename} can't open.");
123
        }
124
125
        return $this->openFromStream($handle, $options);
126 8
    }
127
128 8
    /**
129 2
     * Open zip archive from raw string data.
130
     *
131
     * @param string $data
132 6
     * @param array  $options
133
     *
134
     * @throws ZipException if can't open temp stream
135 6
     *
136 6
     * @return ZipFile
137
     */
138 6
    public function openFromString($data, array $options = [])
139
    {
140
        if ($data === null || $data === '') {
141
            throw new InvalidArgumentException('Empty string passed');
142
        }
143
144
        if (!($handle = fopen('php://temp', 'r+b'))) {
145
            // @codeCoverageIgnoreStart
146
            throw new ZipException("Can't open temp stream.");
147
            // @codeCoverageIgnoreEnd
148
        }
149
        fwrite($handle, $data);
150
        rewind($handle);
151 98
152
        return $this->openFromStream($handle, $options);
153 98
    }
154 91
155
    /**
156 85
     * Open zip archive from stream resource.
157
     *
158
     * @param resource $handle
159
     * @param array    $options
160
     *
161
     * @throws ZipException
162 9
     *
163
     * @return ZipFile
164
     */
165 9
    public function openFromStream($handle, array $options = [])
166
    {
167
        $this->reader = $this->createZipReader($handle, $options);
168
        $this->zipContainer = $this->createZipContainer($this->reader->read());
169
170
        return $this;
171 39
    }
172
173 39
    /**
174
     * @return string[] returns the list files
175
     */
176
    public function getListFiles()
177
    {
178
        // strval is needed to cast entry names to string type
179
        return array_map('strval', array_keys($this->zipContainer->getEntries()));
180
    }
181 3
182
    /**
183 3
     * @return int returns the number of entries in this ZIP file
184
     */
185
    public function count()
186
    {
187
        return $this->zipContainer->count();
188
    }
189
190
    /**
191
     * Returns the file comment.
192
     *
193 4
     * @return string|null the file comment
194
     */
195 4
    public function getArchiveComment()
196
    {
197 3
        return $this->zipContainer->getArchiveComment();
198
    }
199
200
    /**
201
     * Set archive comment.
202
     *
203
     * @param string|null $comment
204
     *
205
     * @return ZipFile
206
     */
207 42
    public function setArchiveComment($comment = null)
208
    {
209 42
        $this->zipContainer->setArchiveComment($comment);
210
211
        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 18
    public function hasEntry($entryName)
222
    {
223 18
        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
    public function getEntry($entryName)
236
    {
237 1
        return $this->zipContainer->getEntry($entryName);
238
    }
239 1
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
    public function isDirectory($entryName)
252 1
    {
253
        return $this->getEntry($entryName)->isDirectory();
254 1
    }
255
256
    /**
257
     * Returns entry comment.
258
     *
259
     * @param string $entryName
260
     *
261
     * @throws ZipException
262
     * @throws ZipEntryNotFoundException
263
     *
264
     * @return string
265
     */
266
    public function getEntryComment($entryName)
267
    {
268 4
        return $this->getEntry($entryName)->getComment();
269
    }
270 4
271
    /**
272 2
     * 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
    public function setEntryComment($entryName, $comment = null)
283
    {
284
        $this->getEntry($entryName)->setComment($comment);
285 49
286
        return $this;
287 49
    }
288
289 48
    /**
290
     * Returns the entry contents.
291
     *
292
     * @param string $entryName
293 48
     *
294
     * @throws ZipEntryNotFoundException
295
     * @throws ZipException
296
     *
297
     * @return string
298
     */
299
    public function getEntryContents($entryName)
300
    {
301
        $zipData = $this->zipContainer->getEntry($entryName)->getData();
302
303
        if ($zipData === null) {
304 4
            throw new ZipException(sprintf('No data for zip entry %s', $entryName));
305
        }
306 4
307 4
        return $zipData->getDataAsString();
308
    }
309 4
310
    /**
311
     * @param string $entryName
312
     *
313
     * @throws ZipEntryNotFoundException
314
     * @throws ZipException
315
     *
316
     * @return resource
317
     */
318
    public function getEntryStream($entryName)
319
    {
320
        $resource = ZipEntryStreamWrapper::wrap($this->zipContainer->getEntry($entryName));
321
        rewind($resource);
322 16
323
        return $resource;
324 16
    }
325
326
    /**
327
     * Get info by entry.
328
     *
329
     * @param string|ZipEntry $entryName
330
     *
331
     * @throws ZipException
332 10
     * @throws ZipEntryNotFoundException
333
     *
334 10
     * @return ZipInfo
335
     */
336 10
    public function getEntryInfo($entryName)
337 10
    {
338
        return new ZipInfo($this->zipContainer->getEntry($entryName));
0 ignored issues
show
Deprecated Code introduced by
The class PhpZip\Model\ZipInfo has been deprecated: Use ZipEntry ( Ignorable by Annotation )

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

338
        return /** @scrutinizer ignore-deprecated */ new ZipInfo($this->zipContainer->getEntry($entryName));
Loading history...
339
    }
340 10
341
    /**
342
     * Get info by all entries.
343
     *
344
     * @return ZipInfo[]
345
     */
346 6
    public function getAllInfo()
347
    {
348 6
        $infoMap = [];
349
350
        foreach ($this->zipContainer->getEntries() as $name => $entry) {
351
            $infoMap[$name] = new ZipInfo($entry);
0 ignored issues
show
Deprecated Code introduced by
The class PhpZip\Model\ZipInfo has been deprecated: Use ZipEntry ( Ignorable by Annotation )

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

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