Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]
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() { 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 print_error('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 public function current() { 513 if (!isset($this->za)) { 514 return false; 515 } 516 517 return $this->get_info($this->pos); 518 } 519 520 /** 521 * Returns the index of current file. 522 * 523 * @return int current file index 524 */ 525 public function key() { 526 return $this->pos; 527 } 528 529 /** 530 * Moves forward to next file. 531 */ 532 public function next() { 533 $this->pos++; 534 } 535 536 /** 537 * Rewinds back to the first file. 538 */ 539 public function rewind() { 540 $this->pos = 0; 541 } 542 543 /** 544 * Did we reach the end? 545 * 546 * @return bool 547 */ 548 public function valid() { 549 if (!isset($this->za)) { 550 return false; 551 } 552 553 // Skip over unwanted system files (get_info will return false). 554 while (!$this->get_info($this->pos) && $this->pos < $this->za->numFiles) { 555 $this->next(); 556 } 557 558 // No files left - we're at the end. 559 if ($this->pos >= $this->za->numFiles) { 560 return false; 561 } 562 563 return true; 564 } 565 566 /** 567 * Create a map of file names used in zip archive. 568 * @return void 569 */ 570 protected function init_namelookup() { 571 if ($this->emptyziphack) { 572 $this->namelookup = array(); 573 return; 574 } 575 576 if (!isset($this->za)) { 577 return; 578 } 579 if (isset($this->namelookup)) { 580 return; 581 } 582 583 $this->namelookup = array(); 584 585 if ($this->mode != file_archive::OPEN) { 586 // No need to tweak existing names when creating zip file because there are none yet! 587 return; 588 } 589 590 if (!file_exists($this->archivepathname)) { 591 return; 592 } 593 594 if (!$fp = fopen($this->archivepathname, 'rb')) { 595 return; 596 } 597 if (!$filesize = filesize($this->archivepathname)) { 598 return; 599 } 600 601 $centralend = self::zip_get_central_end($fp, $filesize); 602 603 if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) { 604 // Single disk archives only and o support for ZIP64, sorry. 605 fclose($fp); 606 return; 607 } 608 609 fseek($fp, $centralend['offset']); 610 $data = fread($fp, $centralend['size']); 611 $pos = 0; 612 $files = array(); 613 for($i=0; $i<$centralend['entries']; $i++) { 614 $file = self::zip_parse_file_header($data, $centralend, $pos); 615 if ($file === false) { 616 // Wrong header, sorry. 617 fclose($fp); 618 return; 619 } 620 $files[] = $file; 621 } 622 fclose($fp); 623 624 foreach ($files as $file) { 625 $name = $file['name']; 626 if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) { 627 // No need to fix ASCII. 628 $name = fix_utf8($name); 629 630 } else if (!($file['general'] & pow(2, 11))) { 631 // First look for unicode name alternatives. 632 $found = false; 633 foreach($file['extra'] as $extra) { 634 if ($extra['id'] === 0x7075) { 635 $data = unpack('cversion/Vcrc', substr($extra['data'], 0, 5)); 636 if ($data['crc'] === crc32($name)) { 637 $found = true; 638 $name = substr($extra['data'], 5); 639 } 640 } 641 } 642 if (!$found and !empty($this->encoding) and $this->encoding !== 'utf-8') { 643 // Try the encoding from open(). 644 $newname = @core_text::convert($name, $this->encoding, 'utf-8'); 645 $original = core_text::convert($newname, 'utf-8', $this->encoding); 646 if ($original === $name) { 647 $found = true; 648 $name = $newname; 649 } 650 } 651 if (!$found and $file['version'] === 0x315) { 652 // This looks like OS X build in zipper. 653 $newname = fix_utf8($name); 654 if ($newname === $name) { 655 $found = true; 656 $name = $newname; 657 } 658 } 659 if (!$found and $file['version'] === 0) { 660 // This looks like our old borked Moodle 2.2 file. 661 $newname = fix_utf8($name); 662 if ($newname === $name) { 663 $found = true; 664 $name = $newname; 665 } 666 } 667 if (!$found and $encoding = get_string('oldcharset', 'langconfig')) { 668 // Last attempt - try the dos/unix encoding from current language. 669 $windows = true; 670 foreach($file['extra'] as $extra) { 671 // In Windows archivers do not usually set any extras with the exception of NTFS flag in WinZip/WinRar. 672 $windows = false; 673 if ($extra['id'] === 0x000a) { 674 $windows = true; 675 break; 676 } 677 } 678 679 if ($windows === true) { 680 switch(strtoupper($encoding)) { 681 case 'ISO-8859-1': $encoding = 'CP850'; break; 682 case 'ISO-8859-2': $encoding = 'CP852'; break; 683 case 'ISO-8859-4': $encoding = 'CP775'; break; 684 case 'ISO-8859-5': $encoding = 'CP866'; break; 685 case 'ISO-8859-6': $encoding = 'CP720'; break; 686 case 'ISO-8859-7': $encoding = 'CP737'; break; 687 case 'ISO-8859-8': $encoding = 'CP862'; break; 688 case 'WINDOWS-1251': $encoding = 'CP866'; break; 689 case 'EUC-JP': 690 case 'UTF-8': 691 if ($winchar = get_string('localewincharset', 'langconfig')) { 692 // Most probably works only for zh_cn, 693 // if there are more problems we could add zipcharset to langconfig files. 694 $encoding = $winchar; 695 } 696 break; 697 } 698 } 699 $newname = @core_text::convert($name, $encoding, 'utf-8'); 700 $original = core_text::convert($newname, 'utf-8', $encoding); 701 702 if ($original === $name) { 703 $name = $newname; 704 } 705 } 706 } 707 $name = str_replace('\\', '/', $name); // no MS \ separators 708 $name = clean_param($name, PARAM_PATH); // only safe chars 709 $name = ltrim($name, '/'); // no leading slash 710 711 if (function_exists('normalizer_normalize')) { 712 $name = normalizer_normalize($name, Normalizer::FORM_C); 713 } 714 715 $this->namelookup[$file['name']] = $name; 716 } 717 } 718 719 /** 720 * Add unicode flag to all files in archive. 721 * 722 * NOTE: single disk archives only, no ZIP64 support. 723 * 724 * @return bool success, modifies the file contents 725 */ 726 protected function fix_utf8_flags() { 727 if ($this->emptyziphack) { 728 return true; 729 } 730 731 if (!file_exists($this->archivepathname)) { 732 return true; 733 } 734 735 // Note: the ZIP structure is described at http://www.pkware.com/documents/casestudies/APPNOTE.TXT 736 if (!$fp = fopen($this->archivepathname, 'rb+')) { 737 return false; 738 } 739 if (!$filesize = filesize($this->archivepathname)) { 740 return false; 741 } 742 743 $centralend = self::zip_get_central_end($fp, $filesize); 744 745 if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) { 746 // Single disk archives only and o support for ZIP64, sorry. 747 fclose($fp); 748 return false; 749 } 750 751 fseek($fp, $centralend['offset']); 752 $data = fread($fp, $centralend['size']); 753 $pos = 0; 754 $files = array(); 755 for($i=0; $i<$centralend['entries']; $i++) { 756 $file = self::zip_parse_file_header($data, $centralend, $pos); 757 if ($file === false) { 758 // Wrong header, sorry. 759 fclose($fp); 760 return false; 761 } 762 763 $newgeneral = $file['general'] | pow(2, 11); 764 if ($newgeneral === $file['general']) { 765 // Nothing to do with this file. 766 continue; 767 } 768 769 if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) { 770 // ASCII file names are always ok. 771 continue; 772 } 773 if ($file['extra']) { 774 // Most probably not created by php zip ext, better to skip it. 775 continue; 776 } 777 if (fix_utf8($file['name']) !== $file['name']) { 778 // Does not look like a valid utf-8 encoded file name, skip it. 779 continue; 780 } 781 782 // Read local file header. 783 fseek($fp, $file['local_offset']); 784 $localfile = unpack('Vsig/vversion_req/vgeneral/vmethod/vmtime/vmdate/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length', fread($fp, 30)); 785 if ($localfile['sig'] !== 0x04034b50) { 786 // Borked file! 787 fclose($fp); 788 return false; 789 } 790 791 $file['local'] = $localfile; 792 $files[] = $file; 793 } 794 795 foreach ($files as $file) { 796 $localfile = $file['local']; 797 // Add the unicode flag in central file header. 798 fseek($fp, $file['central_offset'] + 8); 799 if (ftell($fp) === $file['central_offset'] + 8) { 800 $newgeneral = $file['general'] | pow(2, 11); 801 fwrite($fp, pack('v', $newgeneral)); 802 } 803 // Modify local file header too. 804 fseek($fp, $file['local_offset'] + 6); 805 if (ftell($fp) === $file['local_offset'] + 6) { 806 $newgeneral = $localfile['general'] | pow(2, 11); 807 fwrite($fp, pack('v', $newgeneral)); 808 } 809 } 810 811 fclose($fp); 812 return true; 813 } 814 815 /** 816 * Read end of central signature of ZIP file. 817 * @internal 818 * @static 819 * @param resource $fp 820 * @param int $filesize 821 * @return array|bool 822 */ 823 public static function zip_get_central_end($fp, $filesize) { 824 // Find end of central directory record. 825 fseek($fp, $filesize - 22); 826 $info = unpack('Vsig', fread($fp, 4)); 827 if ($info['sig'] === 0x06054b50) { 828 // There is no comment. 829 fseek($fp, $filesize - 22); 830 $data = fread($fp, 22); 831 } else { 832 // There is some comment with 0xFF max size - that is 65557. 833 fseek($fp, $filesize - 65557); 834 $data = fread($fp, 65557); 835 } 836 837 $pos = strpos($data, pack('V', 0x06054b50)); 838 if ($pos === false) { 839 // Borked ZIP structure! 840 return false; 841 } 842 $centralend = unpack('Vsig/vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_length', substr($data, $pos, 22)); 843 if ($centralend['comment_length']) { 844 $centralend['comment'] = substr($data, 22, $centralend['comment_length']); 845 } else { 846 $centralend['comment'] = ''; 847 } 848 849 return $centralend; 850 } 851 852 /** 853 * Parse file header. 854 * @internal 855 * @param string $data 856 * @param array $centralend 857 * @param int $pos (modified) 858 * @return array|bool file info 859 */ 860 public static function zip_parse_file_header($data, $centralend, &$pos) { 861 $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)); 862 $file['central_offset'] = $centralend['offset'] + $pos; 863 $pos = $pos + 46; 864 if ($file['sig'] !== 0x02014b50) { 865 // Borked ZIP structure! 866 return false; 867 } 868 $file['name'] = substr($data, $pos, $file['name_length']); 869 $pos = $pos + $file['name_length']; 870 $file['extra'] = array(); 871 $file['extra_data'] = ''; 872 if ($file['extra_length']) { 873 $extradata = substr($data, $pos, $file['extra_length']); 874 $file['extra_data'] = $extradata; 875 while (strlen($extradata) > 4) { 876 $extra = unpack('vid/vsize', substr($extradata, 0, 4)); 877 $extra['data'] = substr($extradata, 4, $extra['size']); 878 $extradata = substr($extradata, 4+$extra['size']); 879 $file['extra'][] = $extra; 880 } 881 $pos = $pos + $file['extra_length']; 882 } 883 if ($file['comment_length']) { 884 $pos = $pos + $file['comment_length']; 885 $file['comment'] = substr($data, $pos, $file['comment_length']); 886 } else { 887 $file['comment'] = ''; 888 } 889 return $file; 890 } 891 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body