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