Completed
Push — master ( b3b676...8fdc21 )
by Alexey
06:06 queued 11s
created

ZipFile::addFromString()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8.0032

Importance

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