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 packer. 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_packer.php"); 28 require_once("$CFG->libdir/filestorage/zip_archive.php"); 29 30 /** 31 * Utility class - handles all zipping and unzipping operations. 32 * 33 * @package core_files 34 * @category files 35 * @copyright 2008 Petr Skoda (http://skodak.org) 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class zip_packer extends file_packer { 39 40 /** 41 * Zip files and store the result in file storage. 42 * 43 * @param array $files array with full zip paths (including directory information) 44 * as keys (archivepath=>ospathname or archivepath/subdir=>stored_file or archivepath=>array('content_as_string')) 45 * @param int $contextid context ID 46 * @param string $component component 47 * @param string $filearea file area 48 * @param int $itemid item ID 49 * @param string $filepath file path 50 * @param string $filename file name 51 * @param int $userid user ID 52 * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error 53 * @param file_progress $progress Progress indicator callback or null if not required 54 * @return stored_file|bool false if error stored_file instance if ok 55 */ 56 public function archive_to_storage(array $files, $contextid, 57 $component, $filearea, $itemid, $filepath, $filename, 58 $userid = NULL, $ignoreinvalidfiles=true, file_progress $progress = null) { 59 global $CFG; 60 61 $fs = get_file_storage(); 62 63 check_dir_exists($CFG->tempdir.'/zip'); 64 $tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor'); 65 66 if ($result = $this->archive_to_pathname($files, $tmpfile, $ignoreinvalidfiles, $progress)) { 67 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) { 68 if (!$file->delete()) { 69 @unlink($tmpfile); 70 return false; 71 } 72 } 73 $file_record = new stdClass(); 74 $file_record->contextid = $contextid; 75 $file_record->component = $component; 76 $file_record->filearea = $filearea; 77 $file_record->itemid = $itemid; 78 $file_record->filepath = $filepath; 79 $file_record->filename = $filename; 80 $file_record->userid = $userid; 81 $file_record->mimetype = 'application/zip'; 82 83 $result = $fs->create_file_from_pathname($file_record, $tmpfile); 84 } 85 @unlink($tmpfile); 86 return $result; 87 } 88 89 /** 90 * Zip files and store the result in os file. 91 * 92 * @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string')) 93 * @param string $archivefile path to target zip file 94 * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error 95 * @param file_progress $progress Progress indicator callback or null if not required 96 * @return bool true if file created, false if not 97 */ 98 public function archive_to_pathname(array $files, $archivefile, 99 $ignoreinvalidfiles=true, file_progress $progress = null) { 100 $ziparch = new zip_archive(); 101 if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) { 102 return false; 103 } 104 105 $abort = false; 106 foreach ($files as $archivepath => $file) { 107 $archivepath = trim($archivepath, '/'); 108 109 // Record progress each time around this loop. 110 if ($progress) { 111 $progress->progress(); 112 } 113 114 if (is_null($file)) { 115 // Directories have null as content. 116 if (!$ziparch->add_directory($archivepath.'/')) { 117 debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER); 118 if (!$ignoreinvalidfiles) { 119 $abort = true; 120 break; 121 } 122 } 123 124 } else if (is_string($file)) { 125 if (!$this->archive_pathname($ziparch, $archivepath, $file, $progress)) { 126 debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER); 127 if (!$ignoreinvalidfiles) { 128 $abort = true; 129 break; 130 } 131 } 132 133 } else if (is_array($file)) { 134 $content = reset($file); 135 if (!$ziparch->add_file_from_string($archivepath, $content)) { 136 debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER); 137 if (!$ignoreinvalidfiles) { 138 $abort = true; 139 break; 140 } 141 } 142 143 } else { 144 if (!$this->archive_stored($ziparch, $archivepath, $file, $progress)) { 145 debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER); 146 if (!$ignoreinvalidfiles) { 147 $abort = true; 148 break; 149 } 150 } 151 } 152 } 153 154 if (!$ziparch->close()) { 155 @unlink($archivefile); 156 return false; 157 } 158 159 if ($abort) { 160 @unlink($archivefile); 161 return false; 162 } 163 164 return true; 165 } 166 167 /** 168 * Perform archiving file from stored file. 169 * 170 * @param zip_archive $ziparch zip archive instance 171 * @param string $archivepath file path to archive 172 * @param stored_file $file stored_file object 173 * @param file_progress $progress Progress indicator callback or null if not required 174 * @return bool success 175 */ 176 private function archive_stored($ziparch, $archivepath, $file, file_progress $progress = null) { 177 $result = $file->archive_file($ziparch, $archivepath); 178 if (!$result) { 179 return false; 180 } 181 182 if (!$file->is_directory()) { 183 return true; 184 } 185 186 $baselength = strlen($file->get_filepath()); 187 $fs = get_file_storage(); 188 $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), 189 $file->get_filepath(), true, true); 190 foreach ($files as $file) { 191 // Record progress for each file. 192 if ($progress) { 193 $progress->progress(); 194 } 195 196 $path = $file->get_filepath(); 197 $path = substr($path, $baselength); 198 $path = $archivepath.'/'.$path; 199 if (!$file->is_directory()) { 200 $path = $path.$file->get_filename(); 201 } 202 // Ignore result here, partial zipping is ok for now. 203 $file->archive_file($ziparch, $path); 204 } 205 206 return true; 207 } 208 209 /** 210 * Perform archiving file from file path. 211 * 212 * @param zip_archive $ziparch zip archive instance 213 * @param string $archivepath file path to archive 214 * @param string $file path name of the file 215 * @param file_progress $progress Progress indicator callback or null if not required 216 * @return bool success 217 */ 218 private function archive_pathname($ziparch, $archivepath, $file, 219 file_progress $progress = null) { 220 // Record progress each time this function is called. 221 if ($progress) { 222 $progress->progress(); 223 } 224 225 if (!file_exists($file)) { 226 return false; 227 } 228 229 if (is_file($file)) { 230 if (!is_readable($file)) { 231 return false; 232 } 233 return $ziparch->add_file_from_pathname($archivepath, $file); 234 } 235 if (is_dir($file)) { 236 if ($archivepath !== '') { 237 $ziparch->add_directory($archivepath); 238 } 239 $files = new DirectoryIterator($file); 240 foreach ($files as $file) { 241 if ($file->isDot()) { 242 continue; 243 } 244 $newpath = $archivepath.'/'.$file->getFilename(); 245 $this->archive_pathname($ziparch, $newpath, $file->getPathname(), $progress); 246 } 247 unset($files); // Release file handles. 248 return true; 249 } 250 } 251 252 /** 253 * Unzip file to given file path (real OS filesystem), existing files are overwritten. 254 * 255 * @todo MDL-31048 localise messages 256 * @param string|stored_file $archivefile full pathname of zip file or stored_file instance 257 * @param string $pathname target directory 258 * @param array $onlyfiles only extract files present in the array. The path to files MUST NOT 259 * start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt') 260 * @param file_progress $progress Progress indicator callback or null if not required 261 * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error 262 * details. 263 * @return bool|array list of processed files; false if error 264 */ 265 public function extract_to_pathname($archivefile, $pathname, 266 array $onlyfiles = null, file_progress $progress = null, $returnbool = false) { 267 global $CFG; 268 269 if (!is_string($archivefile)) { 270 return $archivefile->extract_to_pathname($this, $pathname, $progress); 271 } 272 273 $processed = array(); 274 $success = true; 275 276 $pathname = rtrim($pathname, '/'); 277 if (!is_readable($archivefile)) { 278 return false; 279 } 280 $ziparch = new zip_archive(); 281 if (!$ziparch->open($archivefile, file_archive::OPEN)) { 282 return false; 283 } 284 285 // Get the number of files (approx). 286 if ($progress) { 287 $approxmax = $ziparch->estimated_count(); 288 $done = 0; 289 } 290 291 foreach ($ziparch as $info) { 292 // Notify progress. 293 if ($progress) { 294 $progress->progress($done, $approxmax); 295 $done++; 296 } 297 298 $size = $info->size; 299 $name = $info->pathname; 300 $origname = $name; 301 302 // File names cannot end with dots on Windows and trailing dots are replaced with underscore. 303 if ($CFG->ostype === 'WINDOWS') { 304 $name = preg_replace('~([^/]+)\.(/|$)~', '\1_\2', $name); 305 } 306 307 if ($name === '' or array_key_exists($name, $processed)) { 308 // Probably filename collisions caused by filename cleaning/conversion. 309 continue; 310 } else if (is_array($onlyfiles) && !in_array($origname, $onlyfiles)) { 311 // Skipping files which are not in the list. 312 continue; 313 } 314 315 if ($info->is_directory) { 316 $newdir = "$pathname/$name"; 317 // directory 318 if (is_file($newdir) and !unlink($newdir)) { 319 $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise 320 $success = false; 321 continue; 322 } 323 if (is_dir($newdir)) { 324 //dir already there 325 $processed[$name] = true; 326 } else { 327 if (mkdir($newdir, $CFG->directorypermissions, true)) { 328 $processed[$name] = true; 329 } else { 330 $processed[$name] = 'Can not create directory'; // TODO: localise 331 $success = false; 332 } 333 } 334 continue; 335 } 336 337 $parts = explode('/', trim($name, '/')); 338 $filename = array_pop($parts); 339 $newdir = rtrim($pathname.'/'.implode('/', $parts), '/'); 340 341 if (!is_dir($newdir)) { 342 if (!mkdir($newdir, $CFG->directorypermissions, true)) { 343 $processed[$name] = 'Can not create directory'; // TODO: localise 344 $success = false; 345 continue; 346 } 347 } 348 349 $newfile = "$newdir/$filename"; 350 351 if (strpos($newfile, './') > 1 || $name !== $origname) { 352 // The path to the entry contains a directory ending with dot. We cannot use extract_to() due to 353 // upstream PHP bugs #69477, #74619 and #77214. Extract the file from its stream which is slower but 354 // should work even in this case. 355 if (!$fp = fopen($newfile, 'wb')) { 356 $processed[$name] = 'Can not write target file'; // TODO: localise. 357 $success = false; 358 continue; 359 } 360 361 if (!$fz = $ziparch->get_stream($info->index)) { 362 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise. 363 $success = false; 364 fclose($fp); 365 continue; 366 } 367 368 while (!feof($fz)) { 369 $content = fread($fz, 262143); 370 fwrite($fp, $content); 371 } 372 373 fclose($fz); 374 fclose($fp); 375 376 } else { 377 if (!$fz = $ziparch->extract_to($pathname, $info->index)) { 378 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise. 379 $success = false; 380 continue; 381 } 382 } 383 384 // Check that the file was correctly created in the destination. 385 if (!file_exists($newfile)) { 386 $processed[$name] = 'Unknown error during zip extraction (file not created).'; // TODO: localise. 387 $success = false; 388 continue; 389 } 390 391 // Check that the size of extracted file matches the expectation. 392 if (filesize($newfile) !== $size) { 393 $processed[$name] = 'Unknown error during zip extraction (file size mismatch).'; // TODO: localise. 394 $success = false; 395 @unlink($newfile); 396 continue; 397 } 398 399 $processed[$name] = true; 400 } 401 402 $ziparch->close(); 403 404 if ($returnbool) { 405 return $success; 406 } else { 407 return $processed; 408 } 409 } 410 411 /** 412 * Unzip file to given file path (real OS filesystem), existing files are overwritten. 413 * 414 * @todo MDL-31048 localise messages 415 * @param string|stored_file $archivefile full pathname of zip file or stored_file instance 416 * @param int $contextid context ID 417 * @param string $component component 418 * @param string $filearea file area 419 * @param int $itemid item ID 420 * @param string $pathbase file path 421 * @param int $userid user ID 422 * @param file_progress $progress Progress indicator callback or null if not required 423 * @return array|bool list of processed files; false if error 424 */ 425 public function extract_to_storage($archivefile, $contextid, 426 $component, $filearea, $itemid, $pathbase, $userid = NULL, 427 file_progress $progress = null) { 428 global $CFG; 429 430 if (!is_string($archivefile)) { 431 return $archivefile->extract_to_storage($this, $contextid, $component, 432 $filearea, $itemid, $pathbase, $userid, $progress); 433 } 434 435 check_dir_exists($CFG->tempdir.'/zip'); 436 437 $pathbase = trim($pathbase, '/'); 438 $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/'; 439 $fs = get_file_storage(); 440 441 $processed = array(); 442 443 $ziparch = new zip_archive(); 444 if (!$ziparch->open($archivefile, file_archive::OPEN)) { 445 return false; 446 } 447 448 // Get the number of files (approx). 449 if ($progress) { 450 $approxmax = $ziparch->estimated_count(); 451 $done = 0; 452 } 453 454 foreach ($ziparch as $info) { 455 // Notify progress. 456 if ($progress) { 457 $progress->progress($done, $approxmax); 458 $done++; 459 } 460 461 $size = $info->size; 462 $name = $info->pathname; 463 464 if ($name === '' or array_key_exists($name, $processed)) { 465 //probably filename collisions caused by filename cleaning/conversion 466 continue; 467 } 468 469 if ($info->is_directory) { 470 $newfilepath = $pathbase.$name.'/'; 471 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid); 472 $processed[$name] = true; 473 continue; 474 } 475 476 $parts = explode('/', trim($name, '/')); 477 $filename = array_pop($parts); 478 $filepath = $pathbase; 479 if ($parts) { 480 $filepath .= implode('/', $parts).'/'; 481 } 482 483 if ($size < 2097151) { 484 // Small file. 485 if (!$fz = $ziparch->get_stream($info->index)) { 486 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise 487 continue; 488 } 489 $content = ''; 490 while (!feof($fz)) { 491 $content .= fread($fz, 262143); 492 } 493 fclose($fz); 494 if (strlen($content) !== $size) { 495 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise 496 // something went wrong :-( 497 unset($content); 498 continue; 499 } 500 501 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) { 502 if (!$file->delete()) { 503 $processed[$name] = 'Can not delete existing file'; // TODO: localise 504 continue; 505 } 506 } 507 $file_record = new stdClass(); 508 $file_record->contextid = $contextid; 509 $file_record->component = $component; 510 $file_record->filearea = $filearea; 511 $file_record->itemid = $itemid; 512 $file_record->filepath = $filepath; 513 $file_record->filename = $filename; 514 $file_record->userid = $userid; 515 if ($fs->create_file_from_string($file_record, $content)) { 516 $processed[$name] = true; 517 } else { 518 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise 519 } 520 unset($content); 521 continue; 522 523 } else { 524 // large file, would not fit into memory :-( 525 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip'); 526 if (!$fp = fopen($tmpfile, 'wb')) { 527 @unlink($tmpfile); 528 $processed[$name] = 'Can not write temp file'; // TODO: localise 529 continue; 530 } 531 if (!$fz = $ziparch->get_stream($info->index)) { 532 @unlink($tmpfile); 533 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise 534 continue; 535 } 536 while (!feof($fz)) { 537 $content = fread($fz, 262143); 538 fwrite($fp, $content); 539 } 540 fclose($fz); 541 fclose($fp); 542 if (filesize($tmpfile) !== $size) { 543 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise 544 // something went wrong :-( 545 @unlink($tmpfile); 546 continue; 547 } 548 549 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) { 550 if (!$file->delete()) { 551 @unlink($tmpfile); 552 $processed[$name] = 'Can not delete existing file'; // TODO: localise 553 continue; 554 } 555 } 556 $file_record = new stdClass(); 557 $file_record->contextid = $contextid; 558 $file_record->component = $component; 559 $file_record->filearea = $filearea; 560 $file_record->itemid = $itemid; 561 $file_record->filepath = $filepath; 562 $file_record->filename = $filename; 563 $file_record->userid = $userid; 564 if ($fs->create_file_from_pathname($file_record, $tmpfile)) { 565 $processed[$name] = true; 566 } else { 567 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise 568 } 569 @unlink($tmpfile); 570 continue; 571 } 572 } 573 $ziparch->close(); 574 return $processed; 575 } 576 577 /** 578 * Returns array of info about all files in archive. 579 * 580 * @param string|file_archive $archivefile 581 * @return array of file infos 582 */ 583 public function list_files($archivefile) { 584 if (!is_string($archivefile)) { 585 return $archivefile->list_files(); 586 } 587 588 $ziparch = new zip_archive(); 589 if (!$ziparch->open($archivefile, file_archive::OPEN)) { 590 return false; 591 } 592 $list = $ziparch->list_files(); 593 $ziparch->close(); 594 return $list; 595 } 596 597 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body