酒店预订平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2005 regels
58 KiB

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