ZipFile.php 58 KB

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