Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Implementation of zip file archive. 19 * 20 * @package core_files 21 * @copyright 2008 Petr Skoda (http://skodak.org) 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once("$CFG->libdir/filestorage/file_archive.php"); 28 29 /** 30 * Zip file archive class. 31 * 32 * @package core_files 33 * @category files 34 * @copyright 2008 Petr Skoda (http://skodak.org) 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class zip_archive extends file_archive { 38 39 /** @var string Pathname of archive */ 40 protected $archivepathname = null; 41 42 /** @var int archive open mode */ 43 protected $mode = null; 44 45 /** @var int Used memory tracking */ 46 protected $usedmem = 0; 47 48 /** @var int Iteration position */ 49 protected $pos = 0; 50 51 /** @var ZipArchive instance */ 52 protected $za; 53 54 /** @var bool was this archive modified? */ 55 protected $modified = false; 56 57 /** @var array unicode decoding array, created by decoding zip file */ 58 protected $namelookup = null; 59 60 /** @var string base64 encoded contents of empty zip file */ 61 protected static $emptyzipcontent = 'UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA=='; 62 63 /** @var bool ugly hack for broken empty zip handling in < PHP 5.3.10 */ 64 protected $emptyziphack = false; 65 66 /** 67 * Create new zip_archive instance. 68 */ 69 public function __construct() { 70 $this->encoding = null; // Autodetects encoding by default. 71 } 72 73 /** 74 * Open or create archive (depending on $mode). 75 * 76 * @todo MDL-31048 return error message 77 * @param string $archivepathname 78 * @param int $mode OPEN, CREATE or OVERWRITE constant 79 * @param string $encoding archive local paths encoding, empty means autodetect 80 * @return bool success 81 */ 82 public function open($archivepathname, $mode=file_archive::CREATE, $encoding=null) { 83 $this->close(); 84 85 $this->usedmem = 0; 86 $this->pos = 0; 87 $this->encoding = $encoding; 88 $this->mode = $mode; 89 90 $this->za = new ZipArchive(); 91 92 switch($mode) { 93 case file_archive::OPEN: $flags = 0; break; 94 case file_archive::OVERWRITE: $flags = ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE; break; //changed in PHP 5.2.8 95 case file_archive::CREATE: 96 default : $flags = ZIPARCHIVE::CREATE; break; 97 } 98 99 $result = $this->za->open($archivepathname, $flags); 100 101 if ($flags == 0 and $result === ZIPARCHIVE::ER_NOZIP and filesize($archivepathname) === 22) { 102 // Legacy PHP versions < 5.3.10 can not deal with empty zip archives. 103 if (file_get_contents($archivepathname) === base64_decode(self::$emptyzipcontent)) { 104 if ($temp = make_temp_directory('zip')) { 105 $this->emptyziphack = tempnam($temp, 'zip'); 106 $this->za = new ZipArchive(); 107 $result = $this->za->open($this->emptyziphack, ZIPARCHIVE::CREATE); 108 } 109 } 110 } 111 112 if ($result === true) { 113 if (file_exists($archivepathname)) { 114 $this->archivepathname = realpath($archivepathname); 115 } else { 116 $this->archivepathname = $archivepathname; 117 } 118 return true; 119 120 } else { 121 $message = 'Unknown error.'; 122 switch ($result) { 123 case ZIPARCHIVE::ER_EXISTS: $message = 'File already exists.'; break; 124 case ZIPARCHIVE::ER_INCONS: $message = 'Zip archive inconsistent.'; break; 125 case ZIPARCHIVE::ER_INVAL: $message = 'Invalid argument.'; break; 126 case ZIPARCHIVE::ER_MEMORY: $message = 'Malloc failure.'; break; 127 case ZIPARCHIVE::ER_NOENT: $message = 'No such file.'; break; 128 case ZIPARCHIVE::ER_NOZIP: $message = 'Not a zip archive.'; break; 129 case ZIPARCHIVE::ER_OPEN: $message = 'Can\'t open file.'; break; 130 case ZIPARCHIVE::ER_READ: $message = 'Read error.'; break; 131 case ZIPARCHIVE::ER_SEEK: $message = 'Seek error.'; break; 132 } 133 debugging($message.': '.$archivepathname, DEBUG_DEVELOPER); 134 $this->za = null; 135 $this->archivepathname = null; 136 return false; 137 } 138 } 139 140 /** 141 * Normalize $localname, always keep in utf-8 encoding. 142 * 143 * @param string $localname name of file in utf-8 encoding 144 * @return string normalised compressed file or directory name 145 */ 146 protected function mangle_pathname($localname) { 147 $result = str_replace('\\', '/', $localname); // no MS \ separators 148 $result = preg_replace('/\.\.+\//', '', $result); // Cleanup any potential ../ transversal (any number of dots). 149 $result = preg_replace('/\.\.+/', '.', $result); // Join together any number of consecutive dots. 150 $result = ltrim($result, '/'); // no leading slash 151 152 if ($result === '.') { 153 $result = ''; 154 } 155 156 return $result; 157 } 158 159 /** 160 * Tries to convert $localname into utf-8 161 * please note that it may fail really badly. 162 * The resulting file name is cleaned. 163 * 164 * @param string $localname name (encoding is read from zip file or guessed) 165 * @return string in utf-8 166 */ 167 protected function unmangle_pathname($localname) { 168 $this->init_namelookup(); 169 170 if (!isset($this->namelookup[$localname])) { 171 $name = $localname; 172 // This should not happen. 173 if (!empty($this->encoding) and $this->encoding !== 'utf-8') { 174 $name = @core_text::convert($name, $this->encoding, 'utf-8'); 175 } 176 $name = str_replace('\\', '/', $name); // no MS \ separators 177 $name = clean_param($name, PARAM_PATH); // only safe chars 178 return ltrim($name, '/'); // no leading slash 179 } 180 181 return $this->namelookup[$localname]; 182 } 183 184 /** 185 * Close archive, write changes to disk. 186 * 187 * @return bool success 188 */ 189 public function close() { 190 if (!isset($this->za)) { 191 return false; 192 } 193 194 if ($this->emptyziphack) { 195 @$this->za->close(); 196 $this->za = null; 197 $this->mode = null; 198 $this->namelookup = null; 199 $this->modified = false; 200 @unlink($this->emptyziphack); 201 $this->emptyziphack = false; 202 return true; 203 204 } else if ($this->za->numFiles == 0) { 205 // PHP can not create empty archives, so let's fake it. 206 @$this->za->close(); 207 $this->za = null; 208 $this->mode = null; 209 $this->namelookup = null; 210 $this->modified = false; 211 // If the existing archive is already empty, we didn't change it. Don't bother completing a save. 212 // This is important when we are inspecting archives that we might not have write permission to. 213 if (@filesize($this->archivepathname) == 22 && 214 @file_get_contents($this->archivepathname) === base64_decode(self::$emptyzipcontent)) { 215 return true; 216 } 217 @unlink($this->archivepathname); 218 $data = base64_decode(self::$emptyzipcontent); 219 if (!file_put_contents($this->archivepathname, $data)) { 220 return false; 221 } 222 return true; 223 } 224 225 $res = $this->za->close(); 226 $this->za = null; 227 $this->mode = null; 228 $this->namelookup = null; 229 230 if ($this->modified) { 231 $this->fix_utf8_flags(); 232 $this->modified = false; 233 } 234 235 return $res; 236 } 237 238 /** 239 * Returns file stream for reading of content. 240 * 241 * @param int $index index of file 242 * @return resource|bool file handle or false if error 243 */ 244 public function get_stream($index) { 245 if (!isset($this->za)) { 246 return false; 247 } 248 249 $name = $this->za->getNameIndex($index); 250 if ($name === false) { 251 return false; 252 } 253 254 return $this->za->getStream($name); 255 } 256 257 /** 258 * Extract the archive contents to the given location. 259 * 260 * @param string $destination Path to the location where to extract the files. 261 * @param int $index Index of the archive entry. 262 * @return bool true on success or false on failure 263 */ 264 public function extract_to($destination, $index) { 265 266 if (!isset($this->za)) { 267 return false; 268 } 269 270 $name = $this->za->getNameIndex($index); 271 272 if ($name === false) { 273 return false; 274 } 275 276 return $this->za->extractTo($destination, $name); 277 } 278 279 /** 280 * Returns file information. 281 * 282 * @param int $index index of file 283 * @return stdClass|bool info object or false if error 284 */ 285 public function get_info($index) { 286 if (!isset($this->za)) { 287 return false; 288 } 289 290 // Need to use the ZipArchive's numfiles, as $this->count() relies on this function to count actual files (skipping OSX junk). 291 if ($index < 0 or $index >=$this->za->numFiles) { 292 return false; 293 } 294 295 // PHP 5.6 introduced encoding guessing logic for file names. To keep consistent behaviour with older versions, 296 // we fall back to obtaining file names as raw unmodified strings. 297 $result = $this->za->statIndex($index, ZipArchive::FL_ENC_RAW); 298 299 if ($result === false) { 300 return false; 301 } 302 303 $info = new stdClass(); 304 $info->index = $index; 305 $info->original_pathname = $result['name']; 306 $info->pathname = $this->unmangle_pathname($result['name']); 307 $info->mtime = (int)$result['mtime']; 308 309 if ($info->pathname[strlen($info->pathname)-1] === '/') { 310 $info->is_directory = true; 311 $info->size = 0; 312 } else { 313 $info->is_directory = false; 314 $info->size = (int)$result['size']; 315 } 316 317 if ($this->is_system_file($info)) { 318 // Don't return system files. 319 return false; 320 } 321 322 return $info; 323 } 324 325 /** 326 * Returns array of info about all files in archive. 327 * 328 * @return array of file infos 329 */ 330 public function list_files() { 331 if (!isset($this->za)) { 332 return false; 333 } 334 335 $infos = array(); 336 337 foreach ($this as $info) { 338 // Simply iterating over $this will give us info only for files we're interested in. 339 array_push($infos, $info); 340 } 341 342 return $infos; 343 } 344 345 public function is_system_file($fileinfo) { 346 if (substr($fileinfo->pathname, 0, 8) === '__MACOSX' or substr($fileinfo->pathname, -9) === '.DS_Store') { 347 // Mac OSX system files. 348 return true; 349 } 350 if (substr($fileinfo->pathname, -9) === 'Thumbs.db') { 351 $stream = $this->za->getStream($fileinfo->pathname); 352 $info = base64_encode(fread($stream, 8)); 353 fclose($stream); 354 if ($info === '0M8R4KGxGuE=') { 355 // It's an OLE Compound File - so it's almost certainly a Windows thumbnail cache. 356 return true; 357 } 358 } 359 return false; 360 } 361 362 /** 363 * Returns number of files in archive. 364 * 365 * @return int number of files 366 */ 367 public function count(): int { 368 if (!isset($this->za)) { 369 return false; 370 } 371 372 return count($this->list_files()); 373 } 374 375 /** 376 * Returns approximate number of files in archive. This may be a slight 377 * overestimate. 378 * 379 * @return int|bool Estimated number of files, or false if not opened 380 */ 381 public function estimated_count() { 382 if (!isset($this->za)) { 383 return false; 384 } 385 386 return $this->za->numFiles; 387 } 388 389 /** 390 * Add file into archive. 391 * 392 * @param string $localname name of file in archive 393 * @param string $pathname location of file 394 * @return bool success 395 */ 396 public function add_file_from_pathname($localname, $pathname) { 397 if ($this->emptyziphack) { 398 $this->close(); 399 $this->open($this->archivepathname, file_archive::OVERWRITE, $this->encoding); 400 } 401 402 if (!isset($this->za)) { 403 return false; 404 } 405 406 if ($this->archivepathname === realpath($pathname)) { 407 // Do not add self into archive. 408 return false; 409 } 410 411 if (!is_readable($pathname) or is_dir($pathname)) { 412 return false; 413 } 414 415 if (is_null($localname)) { 416 $localname = clean_param($pathname, PARAM_PATH); 417 } 418 $localname = trim($localname, '/'); // No leading slashes in archives! 419 $localname = $this->mangle_pathname($localname); 420 421 if ($localname === '') { 422 // Sorry - conversion failed badly. 423 return false; 424 } 425 426 if (!$this->za->addFile($pathname, $localname)) { 427 return false; 428 } 429 $this->modified = true; 430 return true; 431 } 432 433 /** 434 * Add content of string into archive. 435 * 436 * @param string $localname name of file in archive 437 * @param string $contents contents 438 * @return bool success 439 */ 440 public function add_file_from_string($localname, $contents) { 441 if ($this->emptyziphack) { 442 $this->close(); 443 $this->open($this->archivepathname, file_archive::OVERWRITE, $this->encoding); 444 } 445 446 if (!isset($this->za)) { 447 return false; 448 } 449 450 $localname = trim($localname, '/'); // No leading slashes in archives! 451 $localname = $this->mangle_pathname($localname); 452 453 if ($localname === '') { 454 // Sorry - conversion failed badly. 455 return false; 456 } 457 458 if ($this->usedmem > 2097151) { 459 // This prevents running out of memory when adding many large files using strings. 460 $this->close(); 461 $res = $this->open($this->archivepathname, file_archive::OPEN, $this->encoding); 462 if ($res !== true) { 463 throw new \moodle_exception('cannotopenzip'); 464 } 465 } 466 $this->usedmem += strlen($contents); 467 468 if (!$this->za->addFromString($localname, $contents)) { 469 return false; 470 } 471 $this->modified = true; 472 return true; 473 } 474 475 /** 476 * Add empty directory into archive. 477 * 478 * @param string $localname name of file in archive 479 * @return bool success 480 */ 481 public function add_directory($localname) { 482 if ($this->emptyziphack) { 483 $this->close(); 484 $this->open($this->archivepathname, file_archive::OVERWRITE, $this->encoding); 485 } 486 487 if (!isset($this->za)) { 488 return false; 489 } 490 $localname = trim($localname, '/'). '/'; 491 $localname = $this->mangle_pathname($localname); 492 493 if ($localname === '/') { 494 // Sorry - conversion failed badly. 495 return false; 496 } 497 498 if ($localname !== '') { 499 if (!$this->za->addEmptyDir($localname)) { 500 return false; 501 } 502 $this->modified = true; 503 } 504 return true; 505 } 506 507 /** 508 * Returns current file info. 509 * 510 * @return stdClass 511 */ 512 #[\ReturnTypeWillChange] 513 public function current() { 514 if (!isset($this->za)) { 515 return false; 516 } 517 518 return $this->get_info($this->pos); 519 } 520 521 /** 522 * Returns the index of current file. 523 * 524 * @return int current file index 525 */ 526 #[\ReturnTypeWillChange] 527 public function key() { 528 return $this->pos; 529 } 530 531 /** 532 * Moves forward to next file. 533 */ 534 public function next(): void { 535 $this->pos++; 536 } 537 538 /** 539 * Rewinds back to the first file. 540 */ 541 public function rewind(): void { 542 $this->pos = 0; 543 } 544 545 /** 546 * Did we reach the end? 547 * 548 * @return bool 549 */ 550 public function valid(): bool { 551 if (!isset($this->za)) { 552 return false; 553 } 554 555 // Skip over unwanted system files (get_info will return false). 556 while (!$this->get_info($this->pos) && $this->pos < $this->za->numFiles) { 557 $this->next(); 558 } 559 560 // No files left - we're at the end. 561 if ($this->pos >= $this->za->numFiles) { 562 return false; 563 } 564 565 return true; 566 } 567 568 /** 569 * Create a map of file names used in zip archive. 570 * @return void 571 */ 572 protected function init_namelookup() { 573 if ($this->emptyziphack) { 574 $this->namelookup = array(); 575 return; 576 } 577 578 if (!isset($this->za)) { 579 return; 580 } 581 if (isset($this->namelookup)) { 582 return; 583 } 584 585 $this->namelookup = array(); 586 587 if ($this->mode != file_archive::OPEN) { 588 // No need to tweak existing names when creating zip file because there are none yet! 589 return; 590 } 591 592 if (!file_exists($this->archivepathname)) { 593 return; 594 } 595 596 if (!$fp = fopen($this->archivepathname, 'rb')) { 597 return; 598 } 599 if (!$filesize = filesize($this->archivepathname)) { 600 return; 601 } 602 603 $centralend = self::zip_get_central_end($fp, $filesize); 604 605 if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) { 606 // Single disk archives only and o support for ZIP64, sorry. 607 fclose($fp); 608 return; 609 } 610 611 fseek($fp, $centralend['offset']); 612 $data = fread($fp, $centralend['size']); 613 $pos = 0; 614 $files = array(); 615 for($i=0; $i<$centralend['entries']; $i++) { 616 $file = self::zip_parse_file_header($data, $centralend, $pos); 617 if ($file === false) { 618 // Wrong header, sorry. 619 fclose($fp); 620 return; 621 } 622 $files[] = $file; 623 } 624 fclose($fp); 625 626 foreach ($files as $file) { 627 $name = $file['name']; 628 if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) { 629 // No need to fix ASCII. 630 $name = fix_utf8($name); 631 632 } else if (!($file['general'] & pow(2, 11))) { 633 // First look for unicode name alternatives. 634 $found = false; 635 foreach($file['extra'] as $extra) { 636 if ($extra['id'] === 0x7075) { 637 $data = unpack('cversion/Vcrc', substr($extra['data'], 0, 5)); 638 if ($data['crc'] === crc32($name)) { 639 $found = true; 640 $name = substr($extra['data'], 5); 641 } 642 } 643 } 644 if (!$found and !empty($this->encoding) and $this->encoding !== 'utf-8') { 645 // Try the encoding from open(). 646 $newname = @core_text::convert($name, $this->encoding, 'utf-8'); 647 $original = core_text::convert($newname, 'utf-8', $this->encoding); 648 if ($original === $name) { 649 $found = true; 650 $name = $newname; 651 } 652 } 653 if (!$found and $file['version'] === 0x315) { 654 // This looks like OS X build in zipper. 655 $newname = fix_utf8($name); 656 if ($newname === $name) { 657 $found = true; 658 $name = $newname; 659 } 660 } 661 if (!$found and $file['version'] === 0) { 662 // This looks like our old borked Moodle 2.2 file. 663 $newname = fix_utf8($name); 664 if ($newname === $name) { 665 $found = true; 666 $name = $newname; 667 } 668 } 669 if (!$found and $encoding = get_string('oldcharset', 'langconfig')) { 670 // Last attempt - try the dos/unix encoding from current language. 671 $windows = true; 672 foreach($file['extra'] as $extra) { 673 // In Windows archivers do not usually set any extras with the exception of NTFS flag in WinZip/WinRar. 674 $windows = false; 675 if ($extra['id'] === 0x000a) { 676 $windows = true; 677 break; 678 } 679 } 680 681 if ($windows === true) { 682 switch(strtoupper($encoding)) { 683 case 'ISO-8859-1': $encoding = 'CP850'; break; 684 case 'ISO-8859-2': $encoding = 'CP852'; break; 685 case 'ISO-8859-4': $encoding = 'CP775'; break; 686 case 'ISO-8859-5': $encoding = 'CP866'; break; 687 case 'ISO-8859-6': $encoding = 'CP720'; break; 688 case 'ISO-8859-7': $encoding = 'CP737'; break; 689 case 'ISO-8859-8': $encoding = 'CP862'; break; 690 case 'WINDOWS-1251': $encoding = 'CP866'; break; 691 case 'EUC-JP': 692 case 'UTF-8': 693 if ($winchar = get_string('localewincharset', 'langconfig')) { 694 // Most probably works only for zh_cn, 695 // if there are more problems we could add zipcharset to langconfig files. 696 $encoding = $winchar; 697 } 698 break; 699 } 700 } 701 $newname = @core_text::convert($name, $encoding, 'utf-8'); 702 $original = core_text::convert($newname, 'utf-8', $encoding); 703 704 if ($original === $name) { 705 $name = $newname; 706 } 707 } 708 } 709 $name = str_replace('\\', '/', $name); // no MS \ separators 710 $name = clean_param($name, PARAM_PATH); // only safe chars 711 $name = ltrim($name, '/'); // no leading slash 712 713 if (function_exists('normalizer_normalize')) { 714 $name = normalizer_normalize($name, Normalizer::FORM_C); 715 } 716 717 $this->namelookup[$file['name']] = $name; 718 } 719 } 720 721 /** 722 * Add unicode flag to all files in archive. 723 * 724 * NOTE: single disk archives only, no ZIP64 support. 725 * 726 * @return bool success, modifies the file contents 727 */ 728 protected function fix_utf8_flags() { 729 if ($this->emptyziphack) { 730 return true; 731 } 732 733 if (!file_exists($this->archivepathname)) { 734 return true; 735 } 736 737 // Note: the ZIP structure is described at http://www.pkware.com/documents/casestudies/APPNOTE.TXT 738 if (!$fp = fopen($this->archivepathname, 'rb+')) { 739 return false; 740 } 741 if (!$filesize = filesize($this->archivepathname)) { 742 return false; 743 } 744 745 $centralend = self::zip_get_central_end($fp, $filesize); 746 747 if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) { 748 // Single disk archives only and o support for ZIP64, sorry. 749 fclose($fp); 750 return false; 751 } 752 753 fseek($fp, $centralend['offset']); 754 $data = fread($fp, $centralend['size']); 755 $pos = 0; 756 $files = array(); 757 for($i=0; $i<$centralend['entries']; $i++) { 758 $file = self::zip_parse_file_header($data, $centralend, $pos); 759 if ($file === false) { 760 // Wrong header, sorry. 761 fclose($fp); 762 return false; 763 } 764 765 $newgeneral = $file['general'] | pow(2, 11); 766 if ($newgeneral === $file['general']) { 767 // Nothing to do with this file. 768 continue; 769 } 770 771 if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) { 772 // ASCII file names are always ok. 773 continue; 774 } 775 if ($file['extra']) { 776 // Most probably not created by php zip ext, better to skip it. 777 continue; 778 } 779 if (fix_utf8($file['name']) !== $file['name']) { 780 // Does not look like a valid utf-8 encoded file name, skip it. 781 continue; 782 } 783 784 // Read local file header. 785 fseek($fp, $file['local_offset']); 786 $localfile = unpack('Vsig/vversion_req/vgeneral/vmethod/vmtime/vmdate/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length', fread($fp, 30)); 787 if ($localfile['sig'] !== 0x04034b50) { 788 // Borked file! 789 fclose($fp); 790 return false; 791 } 792 793 $file['local'] = $localfile; 794 $files[] = $file; 795 } 796 797 foreach ($files as $file) { 798 $localfile = $file['local']; 799 // Add the unicode flag in central file header. 800 fseek($fp, $file['central_offset'] + 8); 801 if (ftell($fp) === $file['central_offset'] + 8) { 802 $newgeneral = $file['general'] | pow(2, 11); 803 fwrite($fp, pack('v', $newgeneral)); 804 } 805 // Modify local file header too. 806 fseek($fp, $file['local_offset'] + 6); 807 if (ftell($fp) === $file['local_offset'] + 6) { 808 $newgeneral = $localfile['general'] | pow(2, 11); 809 fwrite($fp, pack('v', $newgeneral)); 810 } 811 } 812 813 fclose($fp); 814 return true; 815 } 816 817 /** 818 * Read end of central signature of ZIP file. 819 * @internal 820 * @static 821 * @param resource $fp 822 * @param int $filesize 823 * @return array|bool 824 */ 825 public static function zip_get_central_end($fp, $filesize) { 826 // Find end of central directory record. 827 fseek($fp, $filesize - 22); 828 $info = unpack('Vsig', fread($fp, 4)); 829 if ($info['sig'] === 0x06054b50) { 830 // There is no comment. 831 fseek($fp, $filesize - 22); 832 $data = fread($fp, 22); 833 } else { 834 // There is some comment with 0xFF max size - that is 65557. 835 fseek($fp, $filesize - 65557); 836 $data = fread($fp, 65557); 837 } 838 839 $pos = strpos($data, pack('V', 0x06054b50)); 840 if ($pos === false) { 841 // Borked ZIP structure! 842 return false; 843 } 844 $centralend = unpack('Vsig/vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_length', substr($data, $pos, 22)); 845 if ($centralend['comment_length']) { 846 $centralend['comment'] = substr($data, 22, $centralend['comment_length']); 847 } else { 848 $centralend['comment'] = ''; 849 } 850 851 return $centralend; 852 } 853 854 /** 855 * Parse file header. 856 * @internal 857 * @param string $data 858 * @param array $centralend 859 * @param int $pos (modified) 860 * @return array|bool file info 861 */ 862 public static function zip_parse_file_header($data, $centralend, &$pos) { 863 $file = unpack('Vsig/vversion/vversion_req/vgeneral/vmethod/Vmodified/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length/vcomment_length/vdisk/vattr/Vattrext/Vlocal_offset', substr($data, $pos, 46)); 864 $file['central_offset'] = $centralend['offset'] + $pos; 865 $pos = $pos + 46; 866 if ($file['sig'] !== 0x02014b50) { 867 // Borked ZIP structure! 868 return false; 869 } 870 $file['name'] = substr($data, $pos, $file['name_length']); 871 $pos = $pos + $file['name_length']; 872 $file['extra'] = array(); 873 $file['extra_data'] = ''; 874 if ($file['extra_length']) { 875 $extradata = substr($data, $pos, $file['extra_length']); 876 $file['extra_data'] = $extradata; 877 while (strlen($extradata) > 4) { 878 $extra = unpack('vid/vsize', substr($extradata, 0, 4)); 879 $extra['data'] = substr($extradata, 4, $extra['size']); 880 $extradata = substr($extradata, 4+$extra['size']); 881 $file['extra'][] = $extra; 882 } 883 $pos = $pos + $file['extra_length']; 884 } 885 if ($file['comment_length']) { 886 $pos = $pos + $file['comment_length']; 887 $file['comment'] = substr($data, $pos, $file['comment_length']); 888 } else { 889 $file['comment'] = ''; 890 } 891 return $file; 892 } 893 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body