Passed
Push — master ( 3b7697...25d5dd )
by Alexey
03:48 queued 29s
created

ZipFile::addSplFile()   F

Complexity

Conditions 19
Paths 509

Size

Total Lines 98
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 64
c 0
b 0
f 0
nc 509
nop 3
dl 0
loc 98
rs 1.0319

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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