Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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  }