ZipFile.php 52 KB

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