| 1 | efrain | 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 | }
 |