2
0

ZipFile.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <?php
  2. namespace PhpZip;
  3. use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
  4. use PhpZip\Crypto\WinZipAesEngine;
  5. use PhpZip\Exception\Crc32Exception;
  6. use PhpZip\Exception\IllegalArgumentException;
  7. use PhpZip\Exception\ZipCryptoException;
  8. use PhpZip\Exception\ZipException;
  9. use PhpZip\Exception\ZipNotFoundEntry;
  10. use PhpZip\Exception\ZipUnsupportMethod;
  11. use PhpZip\Extra\WinZipAesEntryExtraField;
  12. use PhpZip\Mapper\OffsetPositionMapper;
  13. use PhpZip\Mapper\PositionMapper;
  14. use PhpZip\Model\ZipEntry;
  15. use PhpZip\Model\ZipInfo;
  16. use PhpZip\Util\PackUtil;
  17. /**
  18. * This class is able to open the .ZIP file in read mode and extract files from it.
  19. *
  20. * Implemented support traditional PKWARE encryption and WinZip AES encryption.
  21. * Implemented support ZIP64.
  22. * Implemented support skip a preamble like the one found in self extracting archives.
  23. *
  24. * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
  25. * @author Ne-Lexa alexey@nelexa.ru
  26. * @license MIT
  27. */
  28. class ZipFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
  29. {
  30. /**
  31. * Input seekable stream resource.
  32. *
  33. * @var resource
  34. */
  35. private $inputStream;
  36. /**
  37. * The total number of bytes in the ZIP archive.
  38. *
  39. * @var int
  40. */
  41. private $length;
  42. /**
  43. * The charset to use for entry names and comments.
  44. *
  45. * @var string
  46. */
  47. private $charset;
  48. /**
  49. * The number of bytes in the preamble of this ZIP file.
  50. *
  51. * @var int
  52. */
  53. private $preamble;
  54. /**
  55. * The number of bytes in the postamble of this ZIP file.
  56. *
  57. * @var int
  58. */
  59. private $postamble;
  60. /**
  61. * Maps entry names to zip entries.
  62. *
  63. * @var ZipEntry[]
  64. */
  65. private $entries;
  66. /**
  67. * The file comment.
  68. *
  69. * @var string
  70. */
  71. private $comment;
  72. /**
  73. * Maps offsets specified in the ZIP file to real offsets in the file.
  74. *
  75. * @var PositionMapper
  76. */
  77. private $mapper;
  78. /**
  79. * Private ZipFile constructor.
  80. *
  81. * @see ZipFile::openFromFile()
  82. * @see ZipFile::openFromString()
  83. * @see ZipFile::openFromStream()
  84. */
  85. private function __construct()
  86. {
  87. $this->mapper = new PositionMapper();
  88. $this->charset = "UTF-8";
  89. }
  90. /**
  91. * Open zip archive from file
  92. *
  93. * @param string $filename
  94. * @return ZipFile
  95. * @throws IllegalArgumentException if file doesn't exists.
  96. * @throws ZipException if can't open file.
  97. */
  98. public static function openFromFile($filename)
  99. {
  100. if (!file_exists($filename)) {
  101. throw new IllegalArgumentException("File $filename can't exists.");
  102. }
  103. if (!($handle = fopen($filename, 'rb'))) {
  104. throw new ZipException("File $filename can't open.");
  105. }
  106. $zipFile = self::openFromStream($handle);
  107. $zipFile->length = filesize($filename);
  108. return $zipFile;
  109. }
  110. /**
  111. * Open zip archive from stream resource
  112. *
  113. * @param resource $handle
  114. * @return ZipFile
  115. * @throws IllegalArgumentException Invalid stream resource
  116. * or resource cannot seekable stream
  117. */
  118. public static function openFromStream($handle)
  119. {
  120. if (!is_resource($handle)) {
  121. throw new IllegalArgumentException("Invalid stream resource.");
  122. }
  123. $meta = stream_get_meta_data($handle);
  124. if (!$meta['seekable']) {
  125. throw new IllegalArgumentException("Resource cannot seekable stream.");
  126. }
  127. $zipFile = new self();
  128. $stats = fstat($handle);
  129. if (isset($stats['size'])) {
  130. $zipFile->length = $stats['size'];
  131. }
  132. $zipFile->checkZipFileSignature($handle);
  133. $numEntries = $zipFile->findCentralDirectory($handle);
  134. $zipFile->mountCentralDirectory($handle, $numEntries);
  135. if ($zipFile->preamble + $zipFile->postamble >= $zipFile->length) {
  136. assert(0 === $numEntries);
  137. $zipFile->checkZipFileSignature($handle);
  138. }
  139. assert(null !== $handle);
  140. assert(null !== $zipFile->charset);
  141. assert(null !== $zipFile->entries);
  142. assert(null !== $zipFile->mapper);
  143. $zipFile->inputStream = $handle;
  144. // Do NOT close stream!
  145. return $zipFile;
  146. }
  147. /**
  148. * Check zip file signature
  149. *
  150. * @param resource $handle
  151. * @throws ZipException if this not .ZIP file.
  152. */
  153. private function checkZipFileSignature($handle)
  154. {
  155. rewind($handle);
  156. $signature = current(unpack('V', fread($handle, 4)));
  157. // Constraint: A ZIP file must start with a Local File Header
  158. // or a (ZIP64) End Of Central Directory Record if it's empty.
  159. if (self::LOCAL_FILE_HEADER_SIG !== $signature && self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature && self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
  160. ) {
  161. throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
  162. }
  163. }
  164. /**
  165. * Positions the file pointer at the first Central File Header.
  166. * Performs some means to check that this is really a ZIP file.
  167. *
  168. * @param resource $handle
  169. * @return int
  170. * @throws ZipException If the file is not compatible to the ZIP File
  171. * Format Specification.
  172. */
  173. private function findCentralDirectory($handle)
  174. {
  175. // Search for End of central directory record.
  176. $max = $this->length - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
  177. $min = $max >= 0xffff ? $max - 0xffff : 0;
  178. for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
  179. fseek($handle, $endOfCentralDirRecordPos, SEEK_SET);
  180. // end of central dir signature 4 bytes (0x06054b50)
  181. if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== current(unpack('V', fread($handle, 4))))
  182. continue;
  183. // Process End Of Central Directory Record.
  184. $data = fread($handle, self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 4);
  185. /**
  186. * @var int $diskNo number of this disk - 2 bytes
  187. * @var int $cdDiskNo number of the disk with the start of the
  188. * central directory - 2 bytes
  189. * @var int $cdEntriesDisk total number of entries in the central
  190. * directory on this disk - 2 bytes
  191. * @var int $cdEntries total number of entries in the central
  192. * directory - 2 bytes
  193. * @var int $cdSize size of the central directory - 4 bytes
  194. * @var int $cdPos offset of start of central directory with
  195. * respect to the starting disk number - 4 bytes
  196. * @var int $commentLen ZIP file comment length - 2 bytes
  197. */
  198. $unpack = unpack('vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLen', $data);
  199. extract($unpack);
  200. if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
  201. throw new ZipException(
  202. "ZIP file spanning/splitting is not supported!"
  203. );
  204. }
  205. // .ZIP file comment (variable size)
  206. if (0 < $commentLen) {
  207. $this->comment = fread($handle, $commentLen);
  208. }
  209. $this->preamble = $endOfCentralDirRecordPos;
  210. $this->postamble = $this->length - ftell($handle);
  211. // Check for ZIP64 End Of Central Directory Locator.
  212. $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
  213. fseek($handle, $endOfCentralDirLocatorPos, SEEK_SET);
  214. // zip64 end of central dir locator
  215. // signature 4 bytes (0x07064b50)
  216. if (
  217. 0 > $endOfCentralDirLocatorPos ||
  218. ftell($handle) === $this->length ||
  219. self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== current(unpack('V', fread($handle, 4)))
  220. ) {
  221. // Seek and check first CFH, probably requiring an offset mapper.
  222. $offset = $endOfCentralDirRecordPos - $cdSize;
  223. fseek($handle, $offset, SEEK_SET);
  224. $offset -= $cdPos;
  225. if (0 !== $offset) {
  226. $this->mapper = new OffsetPositionMapper($offset);
  227. }
  228. return (int)$cdEntries;
  229. }
  230. // number of the disk with the
  231. // start of the zip64 end of
  232. // central directory 4 bytes
  233. $zip64EndOfCentralDirectoryRecordDisk = current(unpack('V', fread($handle, 4)));
  234. // relative offset of the zip64
  235. // end of central directory record 8 bytes
  236. $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($handle, 8));
  237. // total number of disks 4 bytes
  238. $totalDisks = current(unpack('V', fread($handle, 4)));
  239. if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
  240. throw new ZipException("ZIP file spanning/splitting is not supported!");
  241. }
  242. fseek($handle, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
  243. // zip64 end of central dir
  244. // signature 4 bytes (0x06064b50)
  245. $zip64EndOfCentralDirSig = current(unpack('V', fread($handle, 4)));
  246. if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
  247. throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
  248. }
  249. // size of zip64 end of central
  250. // directory record 8 bytes
  251. // version made by 2 bytes
  252. // version needed to extract 2 bytes
  253. fseek($handle, 12, SEEK_CUR);
  254. // number of this disk 4 bytes
  255. $diskNo = current(unpack('V', fread($handle, 4)));
  256. // number of the disk with the
  257. // start of the central directory 4 bytes
  258. $cdDiskNo = current(unpack('V', fread($handle, 4)));
  259. // total number of entries in the
  260. // central directory on this disk 8 bytes
  261. $cdEntriesDisk = PackUtil::unpackLongLE(fread($handle, 8));
  262. // total number of entries in the
  263. // central directory 8 bytes
  264. $cdEntries = PackUtil::unpackLongLE(fread($handle, 8));
  265. if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
  266. throw new ZipException(
  267. "ZIP file spanning/splitting is not supported!");
  268. }
  269. if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
  270. throw new ZipException(
  271. "Total Number Of Entries In The Central Directory out of range!");
  272. }
  273. // size of the central directory 8 bytes
  274. //$cdSize = self::getLongLE($channel);
  275. fseek($handle, 8, SEEK_CUR);
  276. // offset of start of central
  277. // directory with respect to
  278. // the starting disk number 8 bytes
  279. $cdPos = PackUtil::unpackLongLE(fread($handle, 8));
  280. // zip64 extensible data sector (variable size)
  281. fseek($handle, $cdPos, SEEK_SET);
  282. $this->preamble = $zip64EndOfCentralDirectoryRecordPos;
  283. return (int)$cdEntries;
  284. }
  285. // Start recovering file entries from min.
  286. $this->preamble = $min;
  287. $this->postamble = $this->length - $min;
  288. return 0;
  289. }
  290. /**
  291. * Reads the central directory from the given seekable byte channel
  292. * and populates the internal tables with ZipEntry instances.
  293. *
  294. * The ZipEntry's will know all data that can be obtained from the
  295. * central directory alone, but not the data that requires the local
  296. * file header or additional data to be read.
  297. *
  298. * @param resource $handle Input channel.
  299. * @param int $numEntries Size zip entries.
  300. * @throws ZipException
  301. */
  302. private function mountCentralDirectory($handle, $numEntries)
  303. {
  304. $numEntries = (int)$numEntries;
  305. $entries = [];
  306. for (; ; $numEntries--) {
  307. // central file header signature 4 bytes (0x02014b50)
  308. if (self::CENTRAL_FILE_HEADER_SIG !== current(unpack('V', fread($handle, 4)))) {
  309. break;
  310. }
  311. // version made by 2 bytes
  312. $versionMadeBy = current(unpack('v', fread($handle, 2)));
  313. // version needed to extract 2 bytes
  314. fseek($handle, 2, SEEK_CUR);
  315. $unpack = unpack('vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/VrawSize/vfileLen/vextraLen/vcommentLen', fread($handle, 26));
  316. // disk number start 2 bytes
  317. // internal file attributes 2 bytes
  318. fseek($handle, 4, SEEK_CUR);
  319. // external file attributes 4 bytes
  320. // relative offset of local header 4 bytes
  321. $unpack2 = unpack('VrawExternalAttributes/VlfhOff', fread($handle, 8));
  322. $utf8 = 0 !== ($unpack['gpbf'] & ZipEntry::GPBF_UTF8);
  323. if ($utf8) {
  324. $this->charset = "UTF-8";
  325. }
  326. // See appendix D of PKWARE's ZIP File Format Specification.
  327. $name = fread($handle, $unpack['fileLen']);
  328. $entry = new ZipEntry($name, $handle);
  329. $entry->setRawPlatform($versionMadeBy >> 8);
  330. $entry->setGeneralPurposeBitFlags($unpack['gpbf']);
  331. $entry->setRawMethod($unpack['rawMethod']);
  332. $entry->setRawTime($unpack['rawTime']);
  333. $entry->setRawCrc($unpack['rawCrc']);
  334. $entry->setRawCompressedSize($unpack['rawCompressedSize']);
  335. $entry->setRawSize($unpack['rawSize']);
  336. $entry->setRawExternalAttributes($unpack2['rawExternalAttributes']);
  337. $entry->setRawOffset($unpack2['lfhOff']); // must be unmapped!
  338. if (0 < $unpack['extraLen']) {
  339. $entry->setRawExtraFields(fread($handle, $unpack['extraLen']));
  340. }
  341. if (0 < $unpack['commentLen']) {
  342. $entry->setComment(fread($handle, $unpack['commentLen']));
  343. }
  344. unset($unpack, $unpack2);
  345. // Re-load virtual offset after ZIP64 Extended Information
  346. // Extra Field may have been parsed, map it to the real
  347. // offset and conditionally update the preamble size from it.
  348. $lfhOff = $this->mapper->map($entry->getOffset());
  349. if ($lfhOff < $this->preamble) {
  350. $this->preamble = $lfhOff;
  351. }
  352. $entries[$entry->getName()] = $entry;
  353. }
  354. if (0 !== $numEntries % 0x10000) {
  355. throw new ZipException("Expected " . abs($numEntries) .
  356. ($numEntries > 0 ? " more" : " less") .
  357. " entries in the Central Directory!");
  358. }
  359. $this->entries = $entries;
  360. }
  361. /**
  362. * Open zip archive from raw string data.
  363. *
  364. * @param string $data
  365. * @return ZipFile
  366. * @throws IllegalArgumentException if data not available.
  367. * @throws ZipException if can't open temp stream.
  368. */
  369. public static function openFromString($data)
  370. {
  371. if (null === $data || strlen($data) === 0) {
  372. throw new IllegalArgumentException("Data not available");
  373. }
  374. if (!($handle = fopen('php://temp', 'r+b'))) {
  375. throw new ZipException("Can't open temp stream.");
  376. }
  377. fwrite($handle, $data);
  378. rewind($handle);
  379. $zipFile = self::openFromStream($handle);
  380. $zipFile->length = strlen($data);
  381. return $zipFile;
  382. }
  383. /**
  384. * Returns the number of entries in this ZIP file.
  385. *
  386. * @return int
  387. */
  388. public function count()
  389. {
  390. return sizeof($this->entries);
  391. }
  392. /**
  393. * Returns the list files.
  394. *
  395. * @return string[]
  396. */
  397. public function getListFiles()
  398. {
  399. return array_keys($this->entries);
  400. }
  401. /**
  402. * @api
  403. * @return ZipEntry[]
  404. */
  405. public function getRawEntries()
  406. {
  407. return $this->entries;
  408. }
  409. /**
  410. * Checks whether a entry exists
  411. *
  412. * @param string $entryName
  413. * @return bool
  414. */
  415. public function hasEntry($entryName)
  416. {
  417. return isset($this->entries[$entryName]);
  418. }
  419. /**
  420. * Check whether the directory entry.
  421. * Returns true if and only if this ZIP entry represents a directory entry
  422. * (i.e. end with '/').
  423. *
  424. * @param string $entryName
  425. * @return bool
  426. * @throws ZipNotFoundEntry
  427. */
  428. public function isDirectory($entryName)
  429. {
  430. if (!isset($this->entries[$entryName])) {
  431. throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
  432. }
  433. return $this->entries[$entryName]->isDirectory();
  434. }
  435. /**
  436. * Set password to all encrypted entries.
  437. *
  438. * @param string $password Password
  439. */
  440. public function setPassword($password)
  441. {
  442. foreach ($this->entries as $entry) {
  443. if ($entry->isEncrypted()) {
  444. $entry->setPassword($password);
  445. }
  446. }
  447. }
  448. /**
  449. * Set password to concrete zip entry.
  450. *
  451. * @param string $entryName Zip entry name
  452. * @param string $password Password
  453. * @throws ZipNotFoundEntry if don't exist zip entry.
  454. */
  455. public function setEntryPassword($entryName, $password)
  456. {
  457. if (!isset($this->entries[$entryName])) {
  458. throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
  459. }
  460. $entry = $this->entries[$entryName];
  461. if ($entry->isEncrypted()) {
  462. $entry->setPassword($password);
  463. }
  464. }
  465. /**
  466. * Returns the file comment.
  467. *
  468. * @return string The file comment.
  469. */
  470. public function getComment()
  471. {
  472. return null === $this->comment ? '' : $this->decode($this->comment);
  473. }
  474. /**
  475. * Decode charset entry name.
  476. *
  477. * @param string $text
  478. * @return string
  479. */
  480. private function decode($text)
  481. {
  482. $inCharset = mb_detect_encoding($text, mb_detect_order(), true);
  483. if ($inCharset === $this->charset) return $text;
  484. return iconv($inCharset, $this->charset, $text);
  485. }
  486. /**
  487. * Returns entry comment.
  488. *
  489. * @param string $entryName
  490. * @return string
  491. * @throws ZipNotFoundEntry
  492. */
  493. public function getEntryComment($entryName)
  494. {
  495. if (!isset($this->entries[$entryName])) {
  496. throw new ZipNotFoundEntry("Not found entry " . $entryName);
  497. }
  498. return $this->entries[$entryName]->getComment();
  499. }
  500. /**
  501. * Returns the name of the character set which is effectively used for
  502. * decoding entry names and the file comment.
  503. *
  504. * @return string
  505. */
  506. public function getCharset()
  507. {
  508. return $this->charset;
  509. }
  510. /**
  511. * Returns the file length of this ZIP file in bytes.
  512. *
  513. * @return int
  514. */
  515. public function length()
  516. {
  517. return $this->length;
  518. }
  519. /**
  520. * Get info by entry.
  521. *
  522. * @param string|ZipEntry $entryName
  523. * @return ZipInfo
  524. * @throws ZipNotFoundEntry
  525. */
  526. public function getEntryInfo($entryName)
  527. {
  528. if ($entryName instanceof ZipEntry) {
  529. $entryName = $entryName->getName();
  530. }
  531. if (!isset($this->entries[$entryName])) {
  532. throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
  533. }
  534. $entry = $this->entries[$entryName];
  535. return new ZipInfo($entry);
  536. }
  537. /**
  538. * Get info by all entries.
  539. *
  540. * @return ZipInfo[]
  541. */
  542. public function getAllInfo()
  543. {
  544. return array_map([$this, 'getEntryInfo'], $this->entries);
  545. }
  546. /**
  547. * Extract the archive contents
  548. *
  549. * Extract the complete archive or the given files to the specified destination.
  550. *
  551. * @param string $destination Location where to extract the files.
  552. * @param array $entries The entries to extract. It accepts
  553. * either a single entry name or an array of names.
  554. * @return bool
  555. * @throws ZipException
  556. */
  557. public function extractTo($destination, $entries = null)
  558. {
  559. if ($this->entries === null) {
  560. throw new ZipException("Zip entries not initial");
  561. }
  562. if (!file_exists($destination)) {
  563. throw new ZipException("Destination " . $destination . " not found");
  564. }
  565. if (!is_dir($destination)) {
  566. throw new ZipException("Destination is not directory");
  567. }
  568. if (!is_writable($destination)) {
  569. throw new ZipException("Destination is not writable directory");
  570. }
  571. /**
  572. * @var ZipEntry[] $zipEntries
  573. */
  574. if (!empty($entries)) {
  575. if (is_string($entries)) {
  576. $entries = (array)$entries;
  577. }
  578. if (is_array($entries)) {
  579. $flipEntries = array_flip($entries);
  580. $zipEntries = array_filter($this->entries, function ($zipEntry) use ($flipEntries) {
  581. /**
  582. * @var ZipEntry $zipEntry
  583. */
  584. return isset($flipEntries[$zipEntry->getName()]);
  585. });
  586. }
  587. } else {
  588. $zipEntries = $this->entries;
  589. }
  590. $extract = 0;
  591. foreach ($zipEntries AS $entry) {
  592. $file = $destination . DIRECTORY_SEPARATOR . $entry->getName();
  593. if ($entry->isDirectory()) {
  594. if (!is_dir($file)) {
  595. if (!mkdir($file, 0755, true)) {
  596. throw new ZipException("Can not create dir " . $file);
  597. }
  598. chmod($file, 0755);
  599. touch($file, $entry->getTime());
  600. }
  601. continue;
  602. }
  603. $dir = dirname($file);
  604. if (!file_exists($dir)) {
  605. if (!mkdir($dir, 0755, true)) {
  606. throw new ZipException("Can not create dir " . $dir);
  607. }
  608. chmod($dir, 0755);
  609. touch($file, $entry->getTime());
  610. }
  611. if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) {
  612. return false;
  613. }
  614. touch($file, $entry->getTime());
  615. $extract++;
  616. }
  617. return $extract > 0;
  618. }
  619. /**
  620. * Returns an string content of the given entry.
  621. *
  622. * @param string $entryName
  623. * @return string|null
  624. * @throws ZipException
  625. */
  626. public function getEntryContent($entryName)
  627. {
  628. if (!isset($this->entries[$entryName])) {
  629. throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
  630. }
  631. $entry = $this->entries[$entryName];
  632. $pos = $entry->getOffset();
  633. assert(ZipEntry::UNKNOWN !== $pos);
  634. $startPos = $pos = $this->mapper->map($pos);
  635. fseek($this->inputStream, $pos, SEEK_SET);
  636. $localFileHeaderSig = current(unpack('V', fread($this->inputStream, 4)));
  637. if (self::LOCAL_FILE_HEADER_SIG !== $localFileHeaderSig) {
  638. throw new ZipException($entry->getName() . " (expected Local File Header)");
  639. }
  640. fseek($this->inputStream, $pos + self::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS, SEEK_SET);
  641. $unpack = unpack('vfileLen/vextraLen', fread($this->inputStream, 4));
  642. $pos += self::LOCAL_FILE_HEADER_MIN_LEN + $unpack['fileLen'] + $unpack['extraLen'];
  643. assert(ZipEntry::UNKNOWN !== $entry->getCrc());
  644. $check = $entry->isEncrypted();
  645. $method = $entry->getMethod();
  646. $password = $entry->getPassword();
  647. if ($entry->isEncrypted() && empty($password)) {
  648. throw new ZipException("Not set password");
  649. }
  650. // Strong Encryption Specification - WinZip AES
  651. if ($entry->isEncrypted() && ZipEntry::WINZIP_AES === $method) {
  652. fseek($this->inputStream, $pos, SEEK_SET);
  653. $winZipAesEngine = new WinZipAesEngine($entry);
  654. $content = $winZipAesEngine->decrypt($this->inputStream);
  655. // Disable redundant CRC-32 check.
  656. $check = false;
  657. /**
  658. * @var WinZipAesEntryExtraField $field
  659. */
  660. $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
  661. $method = $field->getMethod();
  662. $entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_WINZIP_AES);
  663. } else {
  664. // Get raw entry content
  665. $content = stream_get_contents($this->inputStream, $entry->getCompressedSize(), $pos);
  666. // Traditional PKWARE Decryption
  667. if ($entry->isEncrypted()) {
  668. $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
  669. $content = $zipCryptoEngine->decrypt($content);
  670. $entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_TRADITIONAL);
  671. }
  672. }
  673. if ($check) {
  674. // Check CRC32 in the Local File Header or Data Descriptor.
  675. $localCrc = null;
  676. if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
  677. // The CRC32 is in the Data Descriptor after the compressed
  678. // size.
  679. // Note the Data Descriptor's Signature is optional:
  680. // All newer apps should write it (and so does TrueVFS),
  681. // but older apps might not.
  682. fseek($this->inputStream, $pos + $entry->getCompressedSize(), SEEK_SET);
  683. $localCrc = current(unpack('V', fread($this->inputStream, 4)));
  684. if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
  685. $localCrc = current(unpack('V', fread($this->inputStream, 4)));
  686. }
  687. } else {
  688. fseek($this->inputStream, $startPos + 14, SEEK_SET);
  689. // The CRC32 in the Local File Header.
  690. $localCrc = current(unpack('V', fread($this->inputStream, 4)));
  691. }
  692. if ($entry->getCrc() !== $localCrc) {
  693. throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
  694. }
  695. }
  696. switch ($method) {
  697. case ZipEntry::METHOD_STORED:
  698. break;
  699. case ZipEntry::METHOD_DEFLATED:
  700. $content = gzinflate($content);
  701. break;
  702. case ZipEntry::METHOD_BZIP2:
  703. if (!extension_loaded('bz2')) {
  704. throw new ZipException('Extension bzip2 not install');
  705. }
  706. $content = bzdecompress($content);
  707. break;
  708. default:
  709. throw new ZipUnsupportMethod($entry->getName()
  710. . " (compression method "
  711. . $method
  712. . " is not supported)");
  713. }
  714. if ($check) {
  715. $localCrc = crc32($content);
  716. if ($entry->getCrc() !== $localCrc) {
  717. if ($entry->isEncrypted()) {
  718. throw new ZipCryptoException("Wrong password");
  719. }
  720. throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
  721. }
  722. }
  723. return $content;
  724. }
  725. /**
  726. * Release all resources
  727. */
  728. function __destruct()
  729. {
  730. $this->close();
  731. }
  732. /**
  733. * Close zip archive and release input stream.
  734. */
  735. public function close()
  736. {
  737. $this->length = null;
  738. if ($this->inputStream !== null) {
  739. fclose($this->inputStream);
  740. $this->inputStream = null;
  741. }
  742. }
  743. /**
  744. * Whether a offset exists
  745. * @link http://php.net/manual/en/arrayaccess.offsetexists.php
  746. * @param string $entryName An offset to check for.
  747. * @return boolean true on success or false on failure.
  748. * The return value will be casted to boolean if non-boolean was returned.
  749. */
  750. public function offsetExists($entryName)
  751. {
  752. return isset($this->entries[$entryName]);
  753. }
  754. /**
  755. * Offset to retrieve
  756. * @link http://php.net/manual/en/arrayaccess.offsetget.php
  757. * @param string $entryName The offset to retrieve.
  758. * @return string|null
  759. */
  760. public function offsetGet($entryName)
  761. {
  762. return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null;
  763. }
  764. /**
  765. * Offset to set
  766. * @link http://php.net/manual/en/arrayaccess.offsetset.php
  767. * @param string $entryName The offset to assign the value to.
  768. * @param mixed $value The value to set.
  769. * @throws ZipUnsupportMethod
  770. */
  771. public function offsetSet($entryName, $value)
  772. {
  773. throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
  774. }
  775. /**
  776. * Offset to unset
  777. * @link http://php.net/manual/en/arrayaccess.offsetunset.php
  778. * @param string $entryName The offset to unset.
  779. * @throws ZipUnsupportMethod
  780. */
  781. public function offsetUnset($entryName)
  782. {
  783. throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
  784. }
  785. /**
  786. * Return the current element
  787. * @link http://php.net/manual/en/iterator.current.php
  788. * @return mixed Can return any type.
  789. * @since 5.0.0
  790. */
  791. public function current()
  792. {
  793. return $this->offsetGet($this->key());
  794. }
  795. /**
  796. * Move forward to next element
  797. * @link http://php.net/manual/en/iterator.next.php
  798. * @return void Any returned value is ignored.
  799. * @since 5.0.0
  800. */
  801. public function next()
  802. {
  803. next($this->entries);
  804. }
  805. /**
  806. * Return the key of the current element
  807. * @link http://php.net/manual/en/iterator.key.php
  808. * @return mixed scalar on success, or null on failure.
  809. * @since 5.0.0
  810. */
  811. public function key()
  812. {
  813. return key($this->entries);
  814. }
  815. /**
  816. * Checks if current position is valid
  817. * @link http://php.net/manual/en/iterator.valid.php
  818. * @return boolean The return value will be casted to boolean and then evaluated.
  819. * Returns true on success or false on failure.
  820. * @since 5.0.0
  821. */
  822. public function valid()
  823. {
  824. return $this->offsetExists($this->key());
  825. }
  826. /**
  827. * Rewind the Iterator to the first element
  828. * @link http://php.net/manual/en/iterator.rewind.php
  829. * @return void Any returned value is ignored.
  830. * @since 5.0.0
  831. */
  832. public function rewind()
  833. {
  834. reset($this->entries);
  835. }
  836. }