ZipFile.php 58 KB

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