ZipFile.php 58 KB

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