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