ZipFile.php 49 KB

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