ZipFile.php 50 KB

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