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

ZipFile::normalizeEntryName()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 8
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 17
ccs 5
cts 5
cp 1
crap 4
rs 10
1
<?php
2
3
namespace PhpZip;
4
5
use PhpZip\Constants\UnixStat;
6
use PhpZip\Constants\ZipCompressionLevel;
7
use PhpZip\Constants\ZipCompressionMethod;
8
use PhpZip\Constants\ZipEncryptionMethod;
9
use PhpZip\Constants\ZipOptions;
10
use PhpZip\Constants\ZipPlatform;
11
use PhpZip\Exception\InvalidArgumentException;
12
use PhpZip\Exception\ZipEntryNotFoundException;
13
use PhpZip\Exception\ZipException;
14
use PhpZip\IO\Stream\ResponseStream;
15
use PhpZip\IO\Stream\ZipEntryStreamWrapper;
16
use PhpZip\IO\ZipReader;
17
use PhpZip\IO\ZipWriter;
18
use PhpZip\Model\Data\ZipFileData;
19
use PhpZip\Model\Data\ZipNewData;
20
use PhpZip\Model\ImmutableZipContainer;
21
use PhpZip\Model\ZipContainer;
22
use PhpZip\Model\ZipEntry;
23
use PhpZip\Model\ZipEntryMatcher;
24
use PhpZip\Model\ZipInfo;
25
use PhpZip\Util\FilesUtil;
26
use PhpZip\Util\StringUtil;
27
use Psr\Http\Message\ResponseInterface;
28
use Symfony\Component\Finder\Finder;
29
use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo;
30
31
/**
32
 * Create, open .ZIP files, modify, get info and extract files.
33
 *
34
 * Implemented support traditional PKWARE encryption and WinZip AES encryption.
35
 * Implemented support ZIP64.
36
 * Support ZipAlign functional.
37
 *
38
 * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
39
 *
40
 * @author Ne-Lexa [email protected]
41
 * @license MIT
42
 */
43
class ZipFile implements ZipFileInterface
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

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

334
        return /** @scrutinizer ignore-deprecated */ new ZipInfo($this->zipContainer->getEntry($entryName));
Loading history...
335
    }
336 30
337
    /**
338 30
     * Get info by all entries.
339
     *
340
     * @return ZipInfo[]
341
     */
342
    public function getAllInfo()
343
    {
344
        $infoMap = [];
345
346 13
        foreach ($this->zipContainer->getEntries() as $name => $entry) {
347
            $infoMap[$name] = new ZipInfo($entry);
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

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