Passed
Push — master ( ca068f...f2d295 )
by Alexey
03:12 queued 16s
created

ZipFile::addGlob()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 37
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 1
b 0
f 0
nc 6
nop 5
dl 0
loc 37
rs 8.8333
1
<?php
2
3
/** @noinspection PhpUsageOfSilenceOperatorInspection */
4
5
namespace PhpZip;
6
7
use PhpZip\Exception\InvalidArgumentException;
8
use PhpZip\Exception\ZipEntryNotFoundException;
9
use PhpZip\Exception\ZipException;
10
use PhpZip\Exception\ZipUnsupportMethodException;
11
use PhpZip\Model\Entry\ZipNewEntry;
12
use PhpZip\Model\Entry\ZipNewFileEntry;
13
use PhpZip\Model\ZipEntry;
14
use PhpZip\Model\ZipEntryMatcher;
15
use PhpZip\Model\ZipInfo;
16
use PhpZip\Model\ZipModel;
17
use PhpZip\Stream\ResponseStream;
18
use PhpZip\Stream\ZipInputStream;
19
use PhpZip\Stream\ZipInputStreamInterface;
20
use PhpZip\Stream\ZipOutputStream;
21
use PhpZip\Util\FilesUtil;
22
use PhpZip\Util\StringUtil;
23
use Psr\Http\Message\ResponseInterface;
24
25
/**
26
 * Create, open .ZIP files, modify, get info and extract files.
27
 *
28
 * Implemented support traditional PKWARE encryption and WinZip AES encryption.
29
 * Implemented support ZIP64.
30
 * Implemented support skip a preamble like the one found in self extracting archives.
31
 * Support ZipAlign functional.
32
 *
33
 * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
34
 *
35
 * @author Ne-Lexa [email protected]
36
 * @license MIT
37
 */
38
class ZipFile implements ZipFileInterface
39
{
40
    /** @var int[] allow compression methods */
41
    private static $allowCompressionMethods = [
42
        self::METHOD_STORED,
43
        self::METHOD_DEFLATED,
44
        self::METHOD_BZIP2,
45
        ZipEntry::UNKNOWN,
46
    ];
47
48
    /** @var int[] allow encryption methods */
49
    private static $allowEncryptionMethods = [
50
        self::ENCRYPTION_METHOD_TRADITIONAL,
51
        self::ENCRYPTION_METHOD_WINZIP_AES_128,
52
        self::ENCRYPTION_METHOD_WINZIP_AES_192,
53
        self::ENCRYPTION_METHOD_WINZIP_AES_256,
54
    ];
55
56
    /** @var array default mime types */
57
    private static $defaultMimeTypes = [
58
        'zip' => 'application/zip',
59
        'apk' => 'application/vnd.android.package-archive',
60
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
61
        'jar' => 'application/java-archive',
62
        'epub' => 'application/epub+zip',
63
    ];
64
65
    /** @var ZipInputStreamInterface input seekable input stream */
66
    protected $inputStream;
67
68
    /** @var ZipModel */
69
    protected $zipModel;
70
71
    /**
72
     * ZipFile constructor.
73
     */
74
    public function __construct()
75
    {
76
        $this->zipModel = new ZipModel();
77
    }
78
79
    /**
80
     * Open zip archive from file.
81
     *
82
     * @param string $filename
83
     *
84
     * @throws ZipException if can't open file
85
     *
86
     * @return ZipFileInterface
87
     */
88
    public function openFile($filename)
89
    {
90
        if (!file_exists($filename)) {
91
            throw new ZipException("File {$filename} does not exist.");
92
        }
93
94
        if (!($handle = @fopen($filename, 'rb'))) {
95
            throw new ZipException("File {$filename} can't open.");
96
        }
97
        $this->openFromStream($handle);
98
99
        return $this;
100
    }
101
102
    /**
103
     * Open zip archive from raw string data.
104
     *
105
     * @param string $data
106
     *
107
     * @throws ZipException if can't open temp stream
108
     *
109
     * @return ZipFileInterface
110
     */
111
    public function openFromString($data)
112
    {
113
        if ($data === null || $data === '') {
114
            throw new InvalidArgumentException('Empty string passed');
115
        }
116
117
        if (!($handle = fopen('php://temp', 'r+b'))) {
118
            throw new ZipException("Can't open temp stream.");
119
        }
120
        fwrite($handle, $data);
121
        rewind($handle);
122
        $this->openFromStream($handle);
123
124
        return $this;
125
    }
126
127
    /**
128
     * Open zip archive from stream resource.
129
     *
130
     * @param resource $handle
131
     *
132
     * @throws ZipException
133
     *
134
     * @return ZipFileInterface
135
     */
136
    public function openFromStream($handle)
137
    {
138
        if (!\is_resource($handle)) {
139
            throw new InvalidArgumentException('Invalid stream resource.');
140
        }
141
        $type = get_resource_type($handle);
142
143
        if ($type !== 'stream') {
144
            throw new InvalidArgumentException("Invalid resource type - {$type}.");
145
        }
146
        $meta = stream_get_meta_data($handle);
147
148
        if ($meta['stream_type'] === 'dir') {
149
            throw new InvalidArgumentException("Invalid stream type - {$meta['stream_type']}.");
150
        }
151
152
        if (!$meta['seekable']) {
153
            throw new InvalidArgumentException('Resource cannot seekable stream.');
154
        }
155
        $this->inputStream = new ZipInputStream($handle);
156
        $this->zipModel = $this->inputStream->readZip();
157
158
        return $this;
159
    }
160
161
    /**
162
     * @return string[] returns the list files
163
     */
164
    public function getListFiles()
165
    {
166
        return array_map('strval', array_keys($this->zipModel->getEntries()));
167
    }
168
169
    /**
170
     * @return int returns the number of entries in this ZIP file
171
     */
172
    public function count()
173
    {
174
        return $this->zipModel->count();
175
    }
176
177
    /**
178
     * Returns the file comment.
179
     *
180
     * @return string|null the file comment
181
     */
182
    public function getArchiveComment()
183
    {
184
        return $this->zipModel->getArchiveComment();
185
    }
186
187
    /**
188
     * Set archive comment.
189
     *
190
     * @param string|null $comment
191
     *
192
     * @return ZipFileInterface
193
     */
194
    public function setArchiveComment($comment = null)
195
    {
196
        $this->zipModel->setArchiveComment($comment);
197
198
        return $this;
199
    }
200
201
    /**
202
     * Checks that the entry in the archive is a directory.
203
     * Returns true if and only if this ZIP entry represents a directory entry
204
     * (i.e. end with '/').
205
     *
206
     * @param string $entryName
207
     *
208
     * @throws ZipEntryNotFoundException
209
     *
210
     * @return bool
211
     */
212
    public function isDirectory($entryName)
213
    {
214
        return $this->zipModel->getEntry($entryName)->isDirectory();
215
    }
216
217
    /**
218
     * Returns entry comment.
219
     *
220
     * @param string $entryName
221
     *
222
     * @throws ZipEntryNotFoundException
223
     *
224
     * @return string
225
     */
226
    public function getEntryComment($entryName)
227
    {
228
        return $this->zipModel->getEntry($entryName)->getComment();
229
    }
230
231
    /**
232
     * Set entry comment.
233
     *
234
     * @param string      $entryName
235
     * @param string|null $comment
236
     *
237
     * @throws ZipException
238
     * @throws ZipEntryNotFoundException
239
     *
240
     * @return ZipFileInterface
241
     */
242
    public function setEntryComment($entryName, $comment = null)
243
    {
244
        $this->zipModel->getEntryForChanges($entryName)->setComment($comment);
245
246
        return $this;
247
    }
248
249
    /**
250
     * Returns the entry contents.
251
     *
252
     * @param string $entryName
253
     *
254
     * @throws ZipException
255
     *
256
     * @return string
257
     */
258
    public function getEntryContents($entryName)
259
    {
260
        return $this->zipModel->getEntry($entryName)->getEntryContent();
261
    }
262
263
    /**
264
     * Checks if there is an entry in the archive.
265
     *
266
     * @param string $entryName
267
     *
268
     * @return bool
269
     */
270
    public function hasEntry($entryName)
271
    {
272
        return $this->zipModel->hasEntry($entryName);
273
    }
274
275
    /**
276
     * Get info by entry.
277
     *
278
     * @param string|ZipEntry $entryName
279
     *
280
     * @throws ZipEntryNotFoundException
281
     * @throws ZipException
282
     *
283
     * @return ZipInfo
284
     */
285
    public function getEntryInfo($entryName)
286
    {
287
        return new ZipInfo($this->zipModel->getEntry($entryName));
288
    }
289
290
    /**
291
     * Get info by all entries.
292
     *
293
     * @return ZipInfo[]
294
     */
295
    public function getAllInfo()
296
    {
297
        return array_map([$this, 'getEntryInfo'], $this->zipModel->getEntries());
298
    }
299
300
    /**
301
     * @return ZipEntryMatcher
302
     */
303
    public function matcher()
304
    {
305
        return $this->zipModel->matcher();
306
    }
307
308
    /**
309
     * Extract the archive contents.
310
     *
311
     * Extract the complete archive or the given files to the specified destination.
312
     *
313
     * @param string            $destination location where to extract the files
314
     * @param array|string|null $entries     The entries to extract. It accepts either
315
     *                                       a single entry name or an array of names.
316
     *
317
     * @throws ZipException
318
     *
319
     * @return ZipFileInterface
320
     */
321
    public function extractTo($destination, $entries = null)
322
    {
323
        if (!file_exists($destination)) {
324
            throw new ZipException(sprintf('Destination %s not found', $destination));
325
        }
326
327
        if (!is_dir($destination)) {
328
            throw new ZipException('Destination is not directory');
329
        }
330
331
        if (!is_writable($destination)) {
332
            throw new ZipException('Destination is not writable directory');
333
        }
334
335
        $zipEntries = $this->zipModel->getEntries();
336
337
        if (!empty($entries)) {
338
            if (\is_string($entries)) {
339
                $entries = (array) $entries;
340
            }
341
342
            if (\is_array($entries)) {
0 ignored issues
show
introduced by
The condition is_array($entries) is always true.
Loading history...
343
                $entries = array_unique($entries);
344
                $flipEntries = array_flip($entries);
345
                $zipEntries = array_filter(
346
                    $zipEntries,
347
                    static function (ZipEntry $zipEntry) use ($flipEntries) {
348
                        return isset($flipEntries[$zipEntry->getName()]);
349
                    }
350
                );
351
            }
352
        }
353
354
        foreach ($zipEntries as $entry) {
355
            $entryName = FilesUtil::normalizeZipPath($entry->getName());
356
            $file = $destination . \DIRECTORY_SEPARATOR . $entryName;
357
358
            if ($entry->isDirectory()) {
359
                if (!is_dir($file)) {
360
                    if (!mkdir($file, 0755, true) && !is_dir($file)) {
361
                        throw new ZipException('Can not create dir ' . $file);
362
                    }
363
                    chmod($file, 0755);
364
                    touch($file, $entry->getTime());
365
                }
366
367
                continue;
368
            }
369
            $dir = \dirname($file);
370
371
            if (!is_dir($dir)) {
372
                if (!mkdir($dir, 0755, true) && !is_dir($dir)) {
373
                    throw new ZipException('Can not create dir ' . $dir);
374
                }
375
                chmod($dir, 0755);
376
                touch($dir, $entry->getTime());
377
            }
378
379
            if (file_put_contents($file, $entry->getEntryContent()) === false) {
380
                throw new ZipException('Can not extract file ' . $entry->getName());
381
            }
382
            touch($file, $entry->getTime());
383
        }
384
385
        return $this;
386
    }
387
388
    /**
389
     * Add entry from the string.
390
     *
391
     * @param string   $localName         zip entry name
392
     * @param string   $contents          string contents
393
     * @param int|null $compressionMethod Compression method.
394
     *                                    Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or
395
     *                                    {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method.
396
     *
397
     * @throws ZipException
398
     *
399
     * @return ZipFileInterface
400
     *
401
     * @see ZipFile::METHOD_STORED
402
     * @see ZipFile::METHOD_DEFLATED
403
     * @see ZipFile::METHOD_BZIP2
404
     */
405
    public function addFromString($localName, $contents, $compressionMethod = null)
406
    {
407
        if ($contents === null) {
0 ignored issues
show
introduced by
The condition $contents === null is always false.
Loading history...
408
            throw new InvalidArgumentException('Contents is null');
409
        }
410
411
        if ($localName === null) {
0 ignored issues
show
introduced by
The condition $localName === null is always false.
Loading history...
412
            throw new InvalidArgumentException('Entry name is null');
413
        }
414
        $localName = ltrim((string) $localName, '\\/');
415
416
        if ($localName === '') {
417
            throw new InvalidArgumentException('Empty entry name');
418
        }
419
        $contents = (string) $contents;
420
        $length = \strlen($contents);
421
422
        if ($compressionMethod === null) {
423
            if ($length >= 512) {
424
                $compressionMethod = ZipEntry::UNKNOWN;
425
            } else {
426
                $compressionMethod = self::METHOD_STORED;
427
            }
428
        } elseif (!\in_array($compressionMethod, self::$allowCompressionMethods, true)) {
429
            throw new ZipUnsupportMethodException('Unsupported compression method ' . $compressionMethod);
430
        }
431
        $externalAttributes = 0100644 << 16;
432
433
        $entry = new ZipNewEntry($contents);
434
        $entry->setName($localName);
435
        $entry->setMethod($compressionMethod);
436
        $entry->setTime(time());
437
        $entry->setExternalAttributes($externalAttributes);
438
439
        $this->zipModel->addEntry($entry);
440
441
        return $this;
442
    }
443
444
    /**
445
     * Add entry from the file.
446
     *
447
     * @param string      $filename          destination file
448
     * @param string|null $localName         zip Entry name
449
     * @param int|null    $compressionMethod Compression method.
450
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
451
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
452
     *
453
     * @throws ZipException
454
     *
455
     * @return ZipFileInterface
456
     *
457
     * @see ZipFile::METHOD_STORED
458
     * @see ZipFile::METHOD_DEFLATED
459
     * @see ZipFile::METHOD_BZIP2
460
     */
461
    public function addFile($filename, $localName = null, $compressionMethod = null)
462
    {
463
        $entry = new ZipNewFileEntry($filename);
464
465
        if ($compressionMethod === null) {
466
            if (\function_exists('mime_content_type')) {
467
                /** @noinspection PhpComposerExtensionStubsInspection */
468
                $mimeType = @mime_content_type($filename);
469
                $type = strtok($mimeType, '/');
470
471
                if ($type === 'image') {
472
                    $compressionMethod = self::METHOD_STORED;
473
                } elseif ($type === 'text' && filesize($filename) < 150) {
474
                    $compressionMethod = self::METHOD_STORED;
475
                } else {
476
                    $compressionMethod = ZipEntry::UNKNOWN;
477
                }
478
            } elseif (filesize($filename) >= 512) {
479
                $compressionMethod = ZipEntry::UNKNOWN;
480
            } else {
481
                $compressionMethod = self::METHOD_STORED;
482
            }
483
        } elseif (!\in_array($compressionMethod, self::$allowCompressionMethods, true)) {
484
            throw new ZipUnsupportMethodException('Unsupported compression method ' . $compressionMethod);
485
        }
486
487
        if ($localName === null) {
488
            $localName = basename($filename);
489
        }
490
        $localName = ltrim((string) $localName, '\\/');
491
492
        if ($localName === '') {
493
            throw new InvalidArgumentException('Empty entry name');
494
        }
495
496
        $stat = stat($filename);
497
        $mode = sprintf('%o', $stat['mode']);
498
        $externalAttributes = (octdec($mode) & 0xffff) << 16;
499
500
        $entry->setName($localName);
501
        $entry->setMethod($compressionMethod);
502
        $entry->setTime($stat['mtime']);
503
        $entry->setExternalAttributes($externalAttributes);
504
505
        $this->zipModel->addEntry($entry);
506
507
        return $this;
508
    }
509
510
    /**
511
     * Add entry from the stream.
512
     *
513
     * @param resource $stream            stream resource
514
     * @param string   $localName         zip Entry name
515
     * @param int|null $compressionMethod Compression method.
516
     *                                    Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
517
     *                                    If null, then auto choosing method.
518
     *
519
     * @throws ZipException
520
     *
521
     * @return ZipFileInterface
522
     *
523
     * @see ZipFile::METHOD_STORED
524
     * @see ZipFile::METHOD_DEFLATED
525
     * @see ZipFile::METHOD_BZIP2
526
     */
527
    public function addFromStream($stream, $localName, $compressionMethod = null)
528
    {
529
        if (!\is_resource($stream)) {
530
            throw new InvalidArgumentException('Stream is not resource');
531
        }
532
533
        if ($localName === null) {
0 ignored issues
show
introduced by
The condition $localName === null is always false.
Loading history...
534
            throw new InvalidArgumentException('Entry name is null');
535
        }
536
        $localName = ltrim((string) $localName, '\\/');
537
538
        if ($localName === '') {
539
            throw new InvalidArgumentException('Empty entry name');
540
        }
541
        $fstat = fstat($stream);
542
543
        if ($fstat !== false) {
544
            $mode = sprintf('%o', $fstat['mode']);
545
            $length = $fstat['size'];
546
547
            if ($compressionMethod === null) {
548
                if ($length >= 512) {
549
                    $compressionMethod = ZipEntry::UNKNOWN;
550
                } else {
551
                    $compressionMethod = self::METHOD_STORED;
552
                }
553
            }
554
        } else {
555
            $mode = 010644;
556
        }
557
558
        if ($compressionMethod !== null && !\in_array($compressionMethod, self::$allowCompressionMethods, true)) {
559
            throw new ZipUnsupportMethodException('Unsupported method ' . $compressionMethod);
560
        }
561
562
        $externalAttributes = (octdec($mode) & 0xffff) << 16;
563
564
        $entry = new ZipNewEntry($stream);
565
        $entry->setName($localName);
566
        $entry->setMethod($compressionMethod);
567
        $entry->setTime(time());
568
        $entry->setExternalAttributes($externalAttributes);
569
570
        $this->zipModel->addEntry($entry);
571
572
        return $this;
573
    }
574
575
    /**
576
     * Add an empty directory in the zip archive.
577
     *
578
     * @param string $dirName
579
     *
580
     * @throws ZipException
581
     *
582
     * @return ZipFileInterface
583
     */
584
    public function addEmptyDir($dirName)
585
    {
586
        if ($dirName === null) {
0 ignored issues
show
introduced by
The condition $dirName === null is always false.
Loading history...
587
            throw new InvalidArgumentException('Dir name is null');
588
        }
589
        $dirName = ltrim((string) $dirName, '\\/');
590
591
        if ($dirName === '') {
592
            throw new InvalidArgumentException('Empty dir name');
593
        }
594
        $dirName = rtrim($dirName, '\\/') . '/';
595
        $externalAttributes = 040755 << 16;
596
597
        $entry = new ZipNewEntry();
598
        $entry->setName($dirName);
599
        $entry->setTime(time());
600
        $entry->setMethod(self::METHOD_STORED);
601
        $entry->setSize(0);
602
        $entry->setCompressedSize(0);
603
        $entry->setCrc(0);
604
        $entry->setExternalAttributes($externalAttributes);
605
606
        $this->zipModel->addEntry($entry);
607
608
        return $this;
609
    }
610
611
    /**
612
     * Add directory not recursively to the zip archive.
613
     *
614
     * @param string   $inputDir          Input directory
615
     * @param string   $localPath         add files to this directory, or the root
616
     * @param int|null $compressionMethod Compression method.
617
     *                                    Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
618
     *                                    If null, then auto choosing method.
619
     *
620
     * @throws ZipException
621
     *
622
     * @return ZipFileInterface
623
     */
624
    public function addDir($inputDir, $localPath = '/', $compressionMethod = null)
625
    {
626
        if ($inputDir === null) {
0 ignored issues
show
introduced by
The condition $inputDir === null is always false.
Loading history...
627
            throw new InvalidArgumentException('Input dir is null');
628
        }
629
        $inputDir = (string) $inputDir;
630
631
        if ($inputDir === '') {
632
            throw new InvalidArgumentException('The input directory is not specified');
633
        }
634
635
        if (!is_dir($inputDir)) {
636
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
637
        }
638
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
639
640
        $directoryIterator = new \DirectoryIterator($inputDir);
641
642
        return $this->addFilesFromIterator($directoryIterator, $localPath, $compressionMethod);
643
    }
644
645
    /**
646
     * Add recursive directory to the zip archive.
647
     *
648
     * @param string   $inputDir          Input directory
649
     * @param string   $localPath         add files to this directory, or the root
650
     * @param int|null $compressionMethod Compression method.
651
     *                                    Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
652
     *                                    If null, then auto choosing method.
653
     *
654
     * @throws ZipException
655
     *
656
     * @return ZipFileInterface
657
     *
658
     * @see ZipFile::METHOD_STORED
659
     * @see ZipFile::METHOD_DEFLATED
660
     * @see ZipFile::METHOD_BZIP2
661
     */
662
    public function addDirRecursive($inputDir, $localPath = '/', $compressionMethod = null)
663
    {
664
        if ($inputDir === null) {
0 ignored issues
show
introduced by
The condition $inputDir === null is always false.
Loading history...
665
            throw new InvalidArgumentException('Input dir is null');
666
        }
667
        $inputDir = (string) $inputDir;
668
669
        if ($inputDir === '') {
670
            throw new InvalidArgumentException('The input directory is not specified');
671
        }
672
673
        if (!is_dir($inputDir)) {
674
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
675
        }
676
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
677
678
        $directoryIterator = new \RecursiveDirectoryIterator($inputDir);
679
680
        return $this->addFilesFromIterator($directoryIterator, $localPath, $compressionMethod);
681
    }
682
683
    /**
684
     * Add directories from directory iterator.
685
     *
686
     * @param \Iterator $iterator          directory iterator
687
     * @param string    $localPath         add files to this directory, or the root
688
     * @param int|null  $compressionMethod Compression method.
689
     *                                     Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
690
     *                                     ZipFile::METHOD_BZIP2. If null, then auto choosing method.
691
     *
692
     * @throws ZipException
693
     *
694
     * @return ZipFileInterface
695
     *
696
     * @see ZipFile::METHOD_STORED
697
     * @see ZipFile::METHOD_DEFLATED
698
     * @see ZipFile::METHOD_BZIP2
699
     */
700
    public function addFilesFromIterator(
701
        \Iterator $iterator,
702
        $localPath = '/',
703
        $compressionMethod = null
704
    ) {
705
        $localPath = (string) $localPath;
706
707
        if ($localPath !== '') {
708
            $localPath = trim($localPath, '\\/');
709
        } else {
710
            $localPath = '';
711
        }
712
713
        $iterator = $iterator instanceof \RecursiveIterator ?
714
            new \RecursiveIteratorIterator($iterator) :
715
            new \IteratorIterator($iterator);
716
        /**
717
         * @var string[] $files
718
         * @var string   $path
719
         */
720
        $files = [];
721
722
        foreach ($iterator as $file) {
723
            if ($file instanceof \SplFileInfo) {
724
                if ($file->getBasename() === '..') {
725
                    continue;
726
                }
727
728
                if ($file->getBasename() === '.') {
729
                    $files[] = \dirname($file->getPathname());
730
                } else {
731
                    $files[] = $file->getPathname();
732
                }
733
            }
734
        }
735
736
        if (empty($files)) {
737
            return $this;
738
        }
739
740
        natcasesort($files);
741
        $path = array_shift($files);
742
743
        $this->doAddFiles($path, $files, $localPath, $compressionMethod);
744
745
        return $this;
746
    }
747
748
    /**
749
     * Add files from glob pattern.
750
     *
751
     * @param string      $inputDir          Input directory
752
     * @param string      $globPattern       glob pattern
753
     * @param string|null $localPath         add files to this directory, or the root
754
     * @param int|null    $compressionMethod Compression method.
755
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
756
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
757
     *
758
     * @throws ZipException
759
     *
760
     * @return ZipFileInterface
761
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
762
     */
763
    public function addFilesFromGlob($inputDir, $globPattern, $localPath = '/', $compressionMethod = null)
764
    {
765
        return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod);
766
    }
767
768
    /**
769
     * Add files from glob pattern.
770
     *
771
     * @param string      $inputDir          Input directory
772
     * @param string      $globPattern       glob pattern
773
     * @param string|null $localPath         add files to this directory, or the root
774
     * @param bool        $recursive         recursive search
775
     * @param int|null    $compressionMethod Compression method.
776
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
777
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
778
     *
779
     * @throws ZipException
780
     *
781
     * @return ZipFileInterface
782
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
783
     */
784
    private function addGlob(
785
        $inputDir,
786
        $globPattern,
787
        $localPath = '/',
788
        $recursive = true,
789
        $compressionMethod = null
790
    ) {
791
        if ($inputDir === null) {
0 ignored issues
show
introduced by
The condition $inputDir === null is always false.
Loading history...
792
            throw new InvalidArgumentException('Input dir is null');
793
        }
794
        $inputDir = (string) $inputDir;
795
796
        if ($inputDir === '') {
797
            throw new InvalidArgumentException('The input directory is not specified');
798
        }
799
800
        if (!is_dir($inputDir)) {
801
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
802
        }
803
        $globPattern = (string) $globPattern;
804
805
        if (empty($globPattern)) {
806
            throw new InvalidArgumentException('The glob pattern is not specified');
807
        }
808
809
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
810
        $globPattern = $inputDir . $globPattern;
811
812
        $filesFound = FilesUtil::globFileSearch($globPattern, \GLOB_BRACE, $recursive);
813
814
        if ($filesFound === false || empty($filesFound)) {
815
            return $this;
816
        }
817
818
        $this->doAddFiles($inputDir, $filesFound, $localPath, $compressionMethod);
819
820
        return $this;
821
    }
822
823
    /**
824
     * Add files recursively from glob pattern.
825
     *
826
     * @param string      $inputDir          Input directory
827
     * @param string      $globPattern       glob pattern
828
     * @param string|null $localPath         add files to this directory, or the root
829
     * @param int|null    $compressionMethod Compression method.
830
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
831
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
832
     *
833
     * @throws ZipException
834
     *
835
     * @return ZipFileInterface
836
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
837
     */
838
    public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null)
839
    {
840
        return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod);
841
    }
842
843
    /**
844
     * Add files from regex pattern.
845
     *
846
     * @param string      $inputDir          search files in this directory
847
     * @param string      $regexPattern      regex pattern
848
     * @param string|null $localPath         add files to this directory, or the root
849
     * @param int|null    $compressionMethod Compression method.
850
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
851
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
852
     *
853
     * @throws ZipException
854
     *
855
     * @return ZipFileInterface
856
     *
857
     * @internal param bool $recursive Recursive search
858
     */
859
    public function addFilesFromRegex($inputDir, $regexPattern, $localPath = '/', $compressionMethod = null)
860
    {
861
        return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod);
862
    }
863
864
    /**
865
     * Add files from regex pattern.
866
     *
867
     * @param string      $inputDir          search files in this directory
868
     * @param string      $regexPattern      regex pattern
869
     * @param string|null $localPath         add files to this directory, or the root
870
     * @param bool        $recursive         recursive search
871
     * @param int|null    $compressionMethod Compression method.
872
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
873
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
874
     *
875
     * @throws ZipException
876
     *
877
     * @return ZipFileInterface
878
     */
879
    private function addRegex(
880
        $inputDir,
881
        $regexPattern,
882
        $localPath = '/',
883
        $recursive = true,
884
        $compressionMethod = null
885
    ) {
886
        $regexPattern = (string) $regexPattern;
887
888
        if (empty($regexPattern)) {
889
            throw new InvalidArgumentException('The regex pattern is not specified');
890
        }
891
        $inputDir = (string) $inputDir;
892
893
        if ($inputDir === '') {
894
            throw new InvalidArgumentException('The input directory is not specified');
895
        }
896
897
        if (!is_dir($inputDir)) {
898
            throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir));
899
        }
900
        $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR;
901
902
        $files = FilesUtil::regexFileSearch($inputDir, $regexPattern, $recursive);
903
904
        if (empty($files)) {
905
            return $this;
906
        }
907
908
        $this->doAddFiles($inputDir, $files, $localPath, $compressionMethod);
909
910
        return $this;
911
    }
912
913
    /**
914
     * @param string   $fileSystemDir
915
     * @param array    $files
916
     * @param string   $zipPath
917
     * @param int|null $compressionMethod
918
     *
919
     * @throws ZipException
920
     */
921
    private function doAddFiles($fileSystemDir, array $files, $zipPath, $compressionMethod = null)
922
    {
923
        $fileSystemDir = rtrim($fileSystemDir, '/\\') . \DIRECTORY_SEPARATOR;
924
925
        if (!empty($zipPath) && \is_string($zipPath)) {
926
            $zipPath = trim($zipPath, '\\/') . '/';
927
        } else {
928
            $zipPath = '/';
929
        }
930
931
        /**
932
         * @var string $file
933
         */
934
        foreach ($files as $file) {
935
            $filename = str_replace($fileSystemDir, $zipPath, $file);
936
            $filename = ltrim($filename, '\\/');
937
938
            if (is_dir($file) && FilesUtil::isEmptyDir($file)) {
939
                $this->addEmptyDir($filename);
940
            } elseif (is_file($file)) {
941
                $this->addFile($file, $filename, $compressionMethod);
942
            }
943
        }
944
    }
945
946
    /**
947
     * Add files recursively from regex pattern.
948
     *
949
     * @param string      $inputDir          search files in this directory
950
     * @param string      $regexPattern      regex pattern
951
     * @param string|null $localPath         add files to this directory, or the root
952
     * @param int|null    $compressionMethod Compression method.
953
     *                                       Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or
954
     *                                       ZipFile::METHOD_BZIP2. If null, then auto choosing method.
955
     *
956
     * @throws ZipException
957
     *
958
     * @return ZipFileInterface
959
     *
960
     * @internal param bool $recursive Recursive search
961
     */
962
    public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = '/', $compressionMethod = null)
963
    {
964
        return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod);
965
    }
966
967
    /**
968
     * Add array data to archive.
969
     * Keys is local names.
970
     * Values is contents.
971
     *
972
     * @param array $mapData associative array for added to zip
973
     */
974
    public function addAll(array $mapData)
975
    {
976
        foreach ($mapData as $localName => $content) {
977
            $this[$localName] = $content;
978
        }
979
    }
980
981
    /**
982
     * Rename the entry.
983
     *
984
     * @param string $oldName old entry name
985
     * @param string $newName new entry name
986
     *
987
     * @throws ZipException
988
     *
989
     * @return ZipFileInterface
990
     */
991
    public function rename($oldName, $newName)
992
    {
993
        if ($oldName === null || $newName === null) {
0 ignored issues
show
introduced by
The condition $newName === null is always false.
Loading history...
994
            throw new InvalidArgumentException('name is null');
995
        }
996
        $oldName = ltrim((string) $oldName, '\\/');
997
        $newName = ltrim((string) $newName, '\\/');
998
999
        if ($oldName !== $newName) {
1000
            $this->zipModel->renameEntry($oldName, $newName);
1001
        }
1002
1003
        return $this;
1004
    }
1005
1006
    /**
1007
     * Delete entry by name.
1008
     *
1009
     * @param string $entryName zip Entry name
1010
     *
1011
     * @throws ZipEntryNotFoundException if entry not found
1012
     *
1013
     * @return ZipFileInterface
1014
     */
1015
    public function deleteFromName($entryName)
1016
    {
1017
        $entryName = ltrim((string) $entryName, '\\/');
1018
1019
        if (!$this->zipModel->deleteEntry($entryName)) {
1020
            throw new ZipEntryNotFoundException($entryName);
1021
        }
1022
1023
        return $this;
1024
    }
1025
1026
    /**
1027
     * Delete entries by glob pattern.
1028
     *
1029
     * @param string $globPattern Glob pattern
1030
     *
1031
     * @return ZipFileInterface
1032
     * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
1033
     */
1034
    public function deleteFromGlob($globPattern)
1035
    {
1036
        if ($globPattern === null || !\is_string($globPattern) || empty($globPattern)) {
0 ignored issues
show
introduced by
The condition is_string($globPattern) is always true.
Loading history...
1037
            throw new InvalidArgumentException('The glob pattern is not specified');
1038
        }
1039
        $globPattern = '~' . FilesUtil::convertGlobToRegEx($globPattern) . '~si';
1040
        $this->deleteFromRegex($globPattern);
1041
1042
        return $this;
1043
    }
1044
1045
    /**
1046
     * Delete entries by regex pattern.
1047
     *
1048
     * @param string $regexPattern Regex pattern
1049
     *
1050
     * @return ZipFileInterface
1051
     */
1052
    public function deleteFromRegex($regexPattern)
1053
    {
1054
        if ($regexPattern === null || !\is_string($regexPattern) || empty($regexPattern)) {
0 ignored issues
show
introduced by
The condition is_string($regexPattern) is always true.
Loading history...
1055
            throw new InvalidArgumentException('The regex pattern is not specified');
1056
        }
1057
        $this->matcher()->match($regexPattern)->delete();
1058
1059
        return $this;
1060
    }
1061
1062
    /**
1063
     * Delete all entries.
1064
     *
1065
     * @return ZipFileInterface
1066
     */
1067
    public function deleteAll()
1068
    {
1069
        $this->zipModel->deleteAll();
1070
1071
        return $this;
1072
    }
1073
1074
    /**
1075
     * Set compression level for new entries.
1076
     *
1077
     * @param int $compressionLevel
1078
     *
1079
     * @return ZipFileInterface
1080
     *
1081
     * @see ZipFile::LEVEL_DEFAULT_COMPRESSION
1082
     * @see ZipFile::LEVEL_SUPER_FAST
1083
     * @see ZipFile::LEVEL_FAST
1084
     * @see ZipFile::LEVEL_BEST_COMPRESSION
1085
     */
1086
    public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION)
1087
    {
1088
        if ($compressionLevel < self::LEVEL_DEFAULT_COMPRESSION ||
1089
            $compressionLevel > self::LEVEL_BEST_COMPRESSION
1090
        ) {
1091
            throw new InvalidArgumentException(
1092
                'Invalid compression level. Minimum level ' .
1093
                self::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . self::LEVEL_BEST_COMPRESSION
1094
            );
1095
        }
1096
        $this->matcher()->all()->invoke(
1097
            function ($entry) use ($compressionLevel) {
1098
                $this->setCompressionLevelEntry($entry, $compressionLevel);
1099
            }
1100
        );
1101
1102
        return $this;
1103
    }
1104
1105
    /**
1106
     * @param string $entryName
1107
     * @param int    $compressionLevel
1108
     *
1109
     * @throws ZipException
1110
     *
1111
     * @return ZipFileInterface
1112
     *
1113
     * @see ZipFile::LEVEL_DEFAULT_COMPRESSION
1114
     * @see ZipFile::LEVEL_SUPER_FAST
1115
     * @see ZipFile::LEVEL_FAST
1116
     * @see ZipFile::LEVEL_BEST_COMPRESSION
1117
     */
1118
    public function setCompressionLevelEntry($entryName, $compressionLevel)
1119
    {
1120
        if ($compressionLevel !== null) {
0 ignored issues
show
introduced by
The condition $compressionLevel !== null is always true.
Loading history...
1121
            if ($compressionLevel < self::LEVEL_DEFAULT_COMPRESSION ||
1122
                $compressionLevel > self::LEVEL_BEST_COMPRESSION
1123
            ) {
1124
                throw new InvalidArgumentException(
1125
                    'Invalid compression level. Minimum level ' .
1126
                    self::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . self::LEVEL_BEST_COMPRESSION
1127
                );
1128
            }
1129
            $entry = $this->zipModel->getEntry($entryName);
1130
1131
            if ($compressionLevel !== $entry->getCompressionLevel()) {
1132
                $entry = $this->zipModel->getEntryForChanges($entry);
1133
                $entry->setCompressionLevel($compressionLevel);
1134
            }
1135
        }
1136
1137
        return $this;
1138
    }
1139
1140
    /**
1141
     * @param string $entryName
1142
     * @param int    $compressionMethod
1143
     *
1144
     * @throws ZipException
1145
     *
1146
     * @return ZipFileInterface
1147
     *
1148
     * @see ZipFile::METHOD_STORED
1149
     * @see ZipFile::METHOD_DEFLATED
1150
     * @see ZipFile::METHOD_BZIP2
1151
     */
1152
    public function setCompressionMethodEntry($entryName, $compressionMethod)
1153
    {
1154
        if (!\in_array($compressionMethod, self::$allowCompressionMethods, true)) {
1155
            throw new ZipUnsupportMethodException('Unsupported method ' . $compressionMethod);
1156
        }
1157
        $entry = $this->zipModel->getEntry($entryName);
1158
1159
        if ($compressionMethod !== $entry->getMethod()) {
1160
            $this->zipModel
1161
                ->getEntryForChanges($entry)
1162
                ->setMethod($compressionMethod)
1163
            ;
1164
        }
1165
1166
        return $this;
1167
    }
1168
1169
    /**
1170
     * zipalign is optimization to Android application (APK) files.
1171
     *
1172
     * @param int|null $align
1173
     *
1174
     * @return ZipFileInterface
1175
     *
1176
     * @see https://developer.android.com/studio/command-line/zipalign.html
1177
     */
1178
    public function setZipAlign($align = null)
1179
    {
1180
        $this->zipModel->setZipAlign($align);
1181
1182
        return $this;
1183
    }
1184
1185
    /**
1186
     * Set password to all input encrypted entries.
1187
     *
1188
     * @param string $password Password
1189
     *
1190
     * @throws ZipException
1191
     *
1192
     * @return ZipFileInterface
1193
     *
1194
     * @deprecated using ZipFile::setReadPassword()
1195
     */
1196
    public function withReadPassword($password)
1197
    {
1198
        return $this->setReadPassword($password);
1199
    }
1200
1201
    /**
1202
     * Set password to all input encrypted entries.
1203
     *
1204
     * @param string $password Password
1205
     *
1206
     * @throws ZipException
1207
     *
1208
     * @return ZipFileInterface
1209
     */
1210
    public function setReadPassword($password)
1211
    {
1212
        $this->zipModel->setReadPassword($password);
1213
1214
        return $this;
1215
    }
1216
1217
    /**
1218
     * Set password to concrete input entry.
1219
     *
1220
     * @param string $entryName
1221
     * @param string $password  Password
1222
     *
1223
     * @throws ZipException
1224
     *
1225
     * @return ZipFileInterface
1226
     */
1227
    public function setReadPasswordEntry($entryName, $password)
1228
    {
1229
        $this->zipModel->setReadPasswordEntry($entryName, $password);
1230
1231
        return $this;
1232
    }
1233
1234
    /**
1235
     * Set password for all entries for update.
1236
     *
1237
     * @param string   $password         If password null then encryption clear
1238
     * @param int|null $encryptionMethod Encryption method
1239
     *
1240
     * @throws ZipException
1241
     *
1242
     * @return ZipFileInterface
1243
     *
1244
     * @deprecated using ZipFile::setPassword()
1245
     */
1246
    public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256)
1247
    {
1248
        return $this->setPassword($password, $encryptionMethod);
1249
    }
1250
1251
    /**
1252
     * Sets a new password for all files in the archive.
1253
     *
1254
     * @param string   $password
1255
     * @param int|null $encryptionMethod Encryption method
1256
     *
1257
     * @throws ZipException
1258
     *
1259
     * @return ZipFileInterface
1260
     */
1261
    public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256)
1262
    {
1263
        $this->zipModel->setWritePassword($password);
1264
1265
        if ($encryptionMethod !== null) {
1266
            if (!\in_array($encryptionMethod, self::$allowEncryptionMethods, true)) {
1267
                throw new ZipException('Invalid encryption method "' . $encryptionMethod . '"');
1268
            }
1269
            $this->zipModel->setEncryptionMethod($encryptionMethod);
1270
        }
1271
1272
        return $this;
1273
    }
1274
1275
    /**
1276
     * Sets a new password of an entry defined by its name.
1277
     *
1278
     * @param string   $entryName
1279
     * @param string   $password
1280
     * @param int|null $encryptionMethod
1281
     *
1282
     * @throws ZipException
1283
     *
1284
     * @return ZipFileInterface
1285
     */
1286
    public function setPasswordEntry($entryName, $password, $encryptionMethod = null)
1287
    {
1288
        if ($encryptionMethod !== null && !\in_array($encryptionMethod, self::$allowEncryptionMethods, true)) {
1289
            throw new ZipException('Invalid encryption method "' . $encryptionMethod . '"');
1290
        }
1291
        $this->matcher()->add($entryName)->setPassword($password, $encryptionMethod);
1292
1293
        return $this;
1294
    }
1295
1296
    /**
1297
     * Remove password for all entries for update.
1298
     *
1299
     * @return ZipFileInterface
1300
     *
1301
     * @deprecated using ZipFile::disableEncryption()
1302
     */
1303
    public function withoutPassword()
1304
    {
1305
        return $this->disableEncryption();
1306
    }
1307
1308
    /**
1309
     * Disable encryption for all entries that are already in the archive.
1310
     *
1311
     * @return ZipFileInterface
1312
     */
1313
    public function disableEncryption()
1314
    {
1315
        $this->zipModel->removePassword();
1316
1317
        return $this;
1318
    }
1319
1320
    /**
1321
     * Disable encryption of an entry defined by its name.
1322
     *
1323
     * @param string $entryName
1324
     *
1325
     * @return ZipFileInterface
1326
     */
1327
    public function disableEncryptionEntry($entryName)
1328
    {
1329
        $this->zipModel->removePasswordEntry($entryName);
1330
1331
        return $this;
1332
    }
1333
1334
    /**
1335
     * Undo all changes done in the archive.
1336
     *
1337
     * @return ZipFileInterface
1338
     */
1339
    public function unchangeAll()
1340
    {
1341
        $this->zipModel->unchangeAll();
1342
1343
        return $this;
1344
    }
1345
1346
    /**
1347
     * Undo change archive comment.
1348
     *
1349
     * @return ZipFileInterface
1350
     */
1351
    public function unchangeArchiveComment()
1352
    {
1353
        $this->zipModel->unchangeArchiveComment();
1354
1355
        return $this;
1356
    }
1357
1358
    /**
1359
     * Revert all changes done to an entry with the given name.
1360
     *
1361
     * @param string|ZipEntry $entry Entry name or ZipEntry
1362
     *
1363
     * @return ZipFileInterface
1364
     */
1365
    public function unchangeEntry($entry)
1366
    {
1367
        $this->zipModel->unchangeEntry($entry);
1368
1369
        return $this;
1370
    }
1371
1372
    /**
1373
     * Save as file.
1374
     *
1375
     * @param string $filename Output filename
1376
     *
1377
     * @throws ZipException
1378
     *
1379
     * @return ZipFileInterface
1380
     */
1381
    public function saveAsFile($filename)
1382
    {
1383
        $filename = (string) $filename;
1384
1385
        $tempFilename = $filename . '.temp' . uniqid('', true);
1386
1387
        if (!($handle = @fopen($tempFilename, 'w+b'))) {
1388
            throw new InvalidArgumentException('File ' . $tempFilename . ' can not open from write.');
1389
        }
1390
        $this->saveAsStream($handle);
1391
1392
        if (!@rename($tempFilename, $filename)) {
1393
            if (is_file($tempFilename)) {
1394
                unlink($tempFilename);
1395
            }
1396
1397
            throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename);
1398
        }
1399
1400
        return $this;
1401
    }
1402
1403
    /**
1404
     * Save as stream.
1405
     *
1406
     * @param resource $handle Output stream resource
1407
     *
1408
     * @throws ZipException
1409
     *
1410
     * @return ZipFileInterface
1411
     */
1412
    public function saveAsStream($handle)
1413
    {
1414
        if (!\is_resource($handle)) {
1415
            throw new InvalidArgumentException('handle is not resource');
1416
        }
1417
        ftruncate($handle, 0);
1418
        $this->writeZipToStream($handle);
1419
        fclose($handle);
1420
1421
        return $this;
1422
    }
1423
1424
    /**
1425
     * Output .ZIP archive as attachment.
1426
     * Die after output.
1427
     *
1428
     * @param string      $outputFilename Output filename
1429
     * @param string|null $mimeType       Mime-Type
1430
     * @param bool        $attachment     Http Header 'Content-Disposition' if true then attachment otherwise inline
1431
     *
1432
     * @throws ZipException
1433
     *
1434
     * @return string
1435
     */
1436
    public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true)
1437
    {
1438
        $outputFilename = (string) $outputFilename;
1439
1440
        if ($mimeType === null) {
1441
            $mimeType = $this->getMimeTypeByFilename($outputFilename);
1442
        }
1443
1444
        if (!($handle = fopen('php://temp', 'w+b'))) {
1445
            throw new InvalidArgumentException('php://temp cannot open for write.');
1446
        }
1447
        $this->writeZipToStream($handle);
1448
        $this->close();
1449
1450
        $size = fstat($handle)['size'];
1451
1452
        $headerContentDisposition = 'Content-Disposition: ' . ($attachment ? 'attachment' : 'inline');
1453
1454
        if (!empty($outputFilename)) {
1455
            $headerContentDisposition .= '; filename="' . basename($outputFilename) . '"';
1456
        }
1457
1458
        header($headerContentDisposition);
1459
        header('Content-Type: ' . $mimeType);
1460
        header('Content-Length: ' . $size);
1461
1462
        rewind($handle);
1463
1464
        try {
1465
            return stream_get_contents($handle, -1, 0);
1466
        } finally {
1467
            fclose($handle);
1468
        }
1469
    }
1470
1471
    /**
1472
     * @param string $outputFilename
1473
     *
1474
     * @return string
1475
     */
1476
    protected function getMimeTypeByFilename($outputFilename)
1477
    {
1478
        $outputFilename = (string) $outputFilename;
1479
        $ext = strtolower(pathinfo($outputFilename, \PATHINFO_EXTENSION));
1480
1481
        if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) {
1482
            return self::$defaultMimeTypes[$ext];
1483
        }
1484
1485
        return self::$defaultMimeTypes['zip'];
1486
    }
1487
1488
    /**
1489
     * Output .ZIP archive as PSR-7 Response.
1490
     *
1491
     * @param ResponseInterface $response       Instance PSR-7 Response
1492
     * @param string            $outputFilename Output filename
1493
     * @param string|null       $mimeType       Mime-Type
1494
     * @param bool              $attachment     Http Header 'Content-Disposition' if true then attachment otherwise inline
1495
     *
1496
     * @throws ZipException
1497
     *
1498
     * @return ResponseInterface
1499
     */
1500
    public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true)
1501
    {
1502
        $outputFilename = (string) $outputFilename;
1503
1504
        if ($mimeType === null) {
1505
            $mimeType = $this->getMimeTypeByFilename($outputFilename);
1506
        }
1507
1508
        if (!($handle = fopen('php://temp', 'w+b'))) {
1509
            throw new InvalidArgumentException('php://temp cannot open for write.');
1510
        }
1511
        $this->writeZipToStream($handle);
1512
        $this->close();
1513
        rewind($handle);
1514
1515
        $contentDispositionValue = ($attachment ? 'attachment' : 'inline');
1516
1517
        if (!empty($outputFilename)) {
1518
            $contentDispositionValue .= '; filename="' . basename($outputFilename) . '"';
1519
        }
1520
1521
        $stream = new ResponseStream($handle);
1522
1523
        return $response
1524
            ->withHeader('Content-Type', $mimeType)
1525
            ->withHeader('Content-Disposition', $contentDispositionValue)
1526
            ->withHeader('Content-Length', $stream->getSize())
1527
            ->withBody($stream)
1528
        ;
1529
    }
1530
1531
    /**
1532
     * @param resource $handle
1533
     *
1534
     * @throws ZipException
1535
     */
1536
    protected function writeZipToStream($handle)
1537
    {
1538
        $this->onBeforeSave();
1539
1540
        $output = new ZipOutputStream($handle, $this->zipModel);
1541
        $output->writeZip();
1542
    }
1543
1544
    /**
1545
     * Returns the zip archive as a string.
1546
     *
1547
     * @throws ZipException
1548
     *
1549
     * @return string
1550
     */
1551
    public function outputAsString()
1552
    {
1553
        if (!($handle = fopen('php://temp', 'w+b'))) {
1554
            throw new InvalidArgumentException('php://temp cannot open for write.');
1555
        }
1556
        $this->writeZipToStream($handle);
1557
        rewind($handle);
1558
1559
        try {
1560
            return stream_get_contents($handle);
1561
        } finally {
1562
            fclose($handle);
1563
        }
1564
    }
1565
1566
    /**
1567
     * Event before save or output.
1568
     */
1569
    protected function onBeforeSave()
1570
    {
1571
    }
1572
1573
    /**
1574
     * Close zip archive and release input stream.
1575
     */
1576
    public function close()
1577
    {
1578
        if ($this->inputStream !== null) {
1579
            $this->inputStream->close();
1580
            $this->inputStream = null;
1581
            $this->zipModel = new ZipModel();
1582
        }
1583
    }
1584
1585
    /**
1586
     * Save and reopen zip archive.
1587
     *
1588
     * @throws ZipException
1589
     *
1590
     * @return ZipFileInterface
1591
     */
1592
    public function rewrite()
1593
    {
1594
        if ($this->inputStream === null) {
1595
            throw new ZipException('input stream is null');
1596
        }
1597
        $meta = stream_get_meta_data($this->inputStream->getStream());
1598
        $content = $this->outputAsString();
1599
        $this->close();
1600
1601
        if ($meta['wrapper_type'] === 'plainfile') {
1602
            /**
1603
             * @var resource $uri
1604
             */
1605
            $uri = $meta['uri'];
1606
1607
            if (file_put_contents($uri, $content) === false) {
0 ignored issues
show
Bug introduced by
$uri of type resource is incompatible with the type string expected by parameter $filename of file_put_contents(). ( Ignorable by Annotation )

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

1607
            if (file_put_contents(/** @scrutinizer ignore-type */ $uri, $content) === false) {
Loading history...
1608
                throw new ZipException("Can not overwrite the zip file in the {$uri} file.");
1609
            }
1610
1611
            if (!($handle = @fopen($uri, 'rb'))) {
0 ignored issues
show
Bug introduced by
$uri of type resource is incompatible with the type string expected by parameter $filename of fopen(). ( Ignorable by Annotation )

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

1611
            if (!($handle = @fopen(/** @scrutinizer ignore-type */ $uri, 'rb'))) {
Loading history...
1612
                throw new ZipException("File {$uri} can't open.");
1613
            }
1614
1615
            return $this->openFromStream($handle);
1616
        }
1617
1618
        return $this->openFromString($content);
1619
    }
1620
1621
    /**
1622
     * Release all resources.
1623
     */
1624
    public function __destruct()
1625
    {
1626
        $this->close();
1627
    }
1628
1629
    /**
1630
     * Offset to set.
1631
     *
1632
     * @see http://php.net/manual/en/arrayaccess.offsetset.php
1633
     *
1634
     * @param string $entryName the offset to assign the value to
1635
     * @param mixed  $contents  the value to set
1636
     *
1637
     * @throws ZipException
1638
     *
1639
     * @see ZipFile::addFromString
1640
     * @see ZipFile::addEmptyDir
1641
     * @see ZipFile::addFile
1642
     * @see ZipFile::addFilesFromIterator
1643
     */
1644
    public function offsetSet($entryName, $contents)
1645
    {
1646
        if ($entryName === null) {
0 ignored issues
show
introduced by
The condition $entryName === null is always false.
Loading history...
1647
            throw new InvalidArgumentException('entryName is null');
1648
        }
1649
        $entryName = ltrim((string) $entryName, '\\/');
1650
1651
        if ($entryName === '') {
1652
            throw new InvalidArgumentException('entryName is empty');
1653
        }
1654
1655
        if ($contents instanceof \SplFileInfo) {
1656
            if ($contents instanceof \DirectoryIterator) {
1657
                $this->addFilesFromIterator($contents, $entryName);
1658
1659
                return;
1660
            }
1661
            $this->addFile($contents->getPathname(), $entryName);
1662
1663
            return;
1664
        }
1665
1666
        if (StringUtil::endsWith($entryName, '/')) {
1667
            $this->addEmptyDir($entryName);
1668
        } elseif (\is_resource($contents)) {
1669
            $this->addFromStream($contents, $entryName);
1670
        } else {
1671
            $this->addFromString($entryName, (string) $contents);
1672
        }
1673
    }
1674
1675
    /**
1676
     * Offset to unset.
1677
     *
1678
     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
1679
     *
1680
     * @param string $entryName the offset to unset
1681
     *
1682
     * @throws ZipEntryNotFoundException
1683
     */
1684
    public function offsetUnset($entryName)
1685
    {
1686
        $this->deleteFromName($entryName);
1687
    }
1688
1689
    /**
1690
     * Return the current element.
1691
     *
1692
     * @see http://php.net/manual/en/iterator.current.php
1693
     *
1694
     * @throws ZipException
1695
     *
1696
     * @return mixed can return any type
1697
     *
1698
     * @since 5.0.0
1699
     */
1700
    public function current()
1701
    {
1702
        return $this->offsetGet($this->key());
1703
    }
1704
1705
    /**
1706
     * Offset to retrieve.
1707
     *
1708
     * @see http://php.net/manual/en/arrayaccess.offsetget.php
1709
     *
1710
     * @param string $entryName the offset to retrieve
1711
     *
1712
     * @throws ZipException
1713
     *
1714
     * @return string|null
1715
     */
1716
    public function offsetGet($entryName)
1717
    {
1718
        return $this->getEntryContents($entryName);
1719
    }
1720
1721
    /**
1722
     * Return the key of the current element.
1723
     *
1724
     * @see http://php.net/manual/en/iterator.key.php
1725
     *
1726
     * @return mixed scalar on success, or null on failure
1727
     *
1728
     * @since 5.0.0
1729
     */
1730
    public function key()
1731
    {
1732
        return key($this->zipModel->getEntries());
1733
    }
1734
1735
    /**
1736
     * Move forward to next element.
1737
     *
1738
     * @see http://php.net/manual/en/iterator.next.php
1739
     * @since 5.0.0
1740
     */
1741
    public function next()
1742
    {
1743
        next($this->zipModel->getEntries());
1744
    }
1745
1746
    /**
1747
     * Checks if current position is valid.
1748
     *
1749
     * @see http://php.net/manual/en/iterator.valid.php
1750
     *
1751
     * @return bool The return value will be casted to boolean and then evaluated.
1752
     *              Returns true on success or false on failure.
1753
     *
1754
     * @since 5.0.0
1755
     */
1756
    public function valid()
1757
    {
1758
        return $this->offsetExists($this->key());
1759
    }
1760
1761
    /**
1762
     * Whether a offset exists.
1763
     *
1764
     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
1765
     *
1766
     * @param string $entryName an offset to check for
1767
     *
1768
     * @return bool true on success or false on failure.
1769
     *              The return value will be casted to boolean if non-boolean was returned.
1770
     */
1771
    public function offsetExists($entryName)
1772
    {
1773
        return $this->hasEntry($entryName);
1774
    }
1775
1776
    /**
1777
     * Rewind the Iterator to the first element.
1778
     *
1779
     * @see http://php.net/manual/en/iterator.rewind.php
1780
     * @since 5.0.0
1781
     */
1782
    public function rewind()
1783
    {
1784
        reset($this->zipModel->getEntries());
1785
    }
1786
}
1787