Differences Between: [Versions 310 and 400] [Versions 39 and 400]
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 .tar.gz packer. 19 * 20 * A limited subset of the .tar format is supported. This packer can open files 21 * that it wrote, but may not be able to open files from other sources, 22 * especially if they use extensions. There are restrictions on file 23 * length and character set of filenames. 24 * 25 * We generate POSIX-compliant ustar files. As a result, the following 26 * restrictions apply to archive paths: 27 * 28 * - Filename may not be more than 100 characters. 29 * - Total of path + filename may not be more than 256 characters. 30 * - For path more than 155 characters it may or may not work. 31 * - May not contain non-ASCII characters. 32 * 33 * @package core_files 34 * @copyright 2013 The Open University 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 38 defined('MOODLE_INTERNAL') || die(); 39 40 require_once("$CFG->libdir/filestorage/file_packer.php"); 41 require_once("$CFG->libdir/filestorage/tgz_extractor.php"); 42 43 /** 44 * Utility class - handles all packing/unpacking of .tar.gz files. 45 * 46 * @package core_files 47 * @category files 48 * @copyright 2013 The Open University 49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 50 */ 51 class tgz_packer extends file_packer { 52 /** 53 * @var int Default timestamp used where unknown (Jan 1st 2013 00:00) 54 */ 55 const DEFAULT_TIMESTAMP = 1356998400; 56 57 /** 58 * @var string Name of special archive index file added by Moodle. 59 */ 60 const ARCHIVE_INDEX_FILE = '.ARCHIVE_INDEX'; 61 62 /** 63 * @var string Required text at start of archive index file before file count. 64 */ 65 const ARCHIVE_INDEX_COUNT_PREFIX = 'Moodle archive file index. Count: '; 66 67 /** 68 * @var bool If true, includes .ARCHIVE_INDEX file in root of tar file. 69 */ 70 protected $includeindex = true; 71 72 /** 73 * @var int Max value for total progress. 74 */ 75 const PROGRESS_MAX = 1000000; 76 77 /** 78 * @var int Tar files have a fixed block size of 512 bytes. 79 */ 80 const TAR_BLOCK_SIZE = 512; 81 82 /** 83 * Archive files and store the result in file storage. 84 * 85 * Any existing file at that location will be overwritten. 86 * 87 * @param array $files array from archive path => pathname or stored_file 88 * @param int $contextid context ID 89 * @param string $component component 90 * @param string $filearea file area 91 * @param int $itemid item ID 92 * @param string $filepath file path 93 * @param string $filename file name 94 * @param int $userid user ID 95 * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error 96 * @param file_progress $progress Progress indicator callback or null if not required 97 * @return stored_file|bool false if error stored_file instance if ok 98 * @throws file_exception If file operations fail 99 * @throws coding_exception If any archive paths do not meet the restrictions 100 */ 101 public function archive_to_storage(array $files, $contextid, 102 $component, $filearea, $itemid, $filepath, $filename, 103 $userid = null, $ignoreinvalidfiles = true, file_progress $progress = null) { 104 global $CFG; 105 106 // Set up a temporary location for the file. 107 $tempfolder = $CFG->tempdir . '/core_files'; 108 check_dir_exists($tempfolder); 109 $tempfile = tempnam($tempfolder, '.tgz'); 110 111 // Archive to the given path. 112 if ($result = $this->archive_to_pathname($files, $tempfile, $ignoreinvalidfiles, $progress)) { 113 // If there is an existing file, delete it. 114 $fs = get_file_storage(); 115 if ($existing = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) { 116 $existing->delete(); 117 } 118 $filerecord = array('contextid' => $contextid, 'component' => $component, 119 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => $filepath, 120 'filename' => $filename, 'userid' => $userid, 'mimetype' => 'application/x-tgz'); 121 self::delete_existing_file_record($fs, $filerecord); 122 $result = $fs->create_file_from_pathname($filerecord, $tempfile); 123 } 124 125 // Delete the temporary file (if created) and return. 126 @unlink($tempfile); 127 return $result; 128 } 129 130 /** 131 * Wrapper function useful for deleting an existing file (if present) just 132 * before creating a new one. 133 * 134 * @param file_storage $fs File storage 135 * @param array $filerecord File record in same format used to create file 136 */ 137 public static function delete_existing_file_record(file_storage $fs, array $filerecord) { 138 if ($existing = $fs->get_file($filerecord['contextid'], $filerecord['component'], 139 $filerecord['filearea'], $filerecord['itemid'], $filerecord['filepath'], 140 $filerecord['filename'])) { 141 $existing->delete(); 142 } 143 } 144 145 /** 146 * By default, the .tar file includes a .ARCHIVE_INDEX file as its first 147 * entry. This makes list_files much faster and allows for better progress 148 * reporting. 149 * 150 * If you need to disable the inclusion of this file, use this function 151 * before calling one of the archive_xx functions. 152 * 153 * @param bool $includeindex If true, includes index 154 */ 155 public function set_include_index($includeindex) { 156 $this->includeindex = $includeindex; 157 } 158 159 /** 160 * Archive files and store the result in an OS file. 161 * 162 * @param array $files array from archive path => pathname or stored_file 163 * @param string $archivefile path to target zip file 164 * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error 165 * @param file_progress $progress Progress indicator callback or null if not required 166 * @return bool true if file created, false if not 167 * @throws coding_exception If any archive paths do not meet the restrictions 168 */ 169 public function archive_to_pathname(array $files, $archivefile, 170 $ignoreinvalidfiles=true, file_progress $progress = null) { 171 // Open .gz file. 172 if (!($gz = gzopen($archivefile, 'wb'))) { 173 return false; 174 } 175 try { 176 // Because we update how we calculate progress after we already 177 // analyse the directory list, we can't just use a number of files 178 // as progress. Instead, progress always goes to PROGRESS_MAX 179 // and we do estimates as a proportion of that. To begin with, 180 // assume that counting files will be 10% of the work, so allocate 181 // one-tenth of PROGRESS_MAX to the total of all files. 182 if ($files) { 183 $progressperfile = (int)(self::PROGRESS_MAX / (count($files) * 10)); 184 } else { 185 // If there are no files, avoid divide by zero. 186 $progressperfile = 1; 187 } 188 $done = 0; 189 190 // Expand the provided files into a complete list of single files. 191 $expandedfiles = array(); 192 foreach ($files as $archivepath => $file) { 193 // Update progress if required. 194 if ($progress) { 195 $progress->progress($done, self::PROGRESS_MAX); 196 } 197 $done += $progressperfile; 198 199 if (is_null($file)) { 200 // Empty directory record. Ensure it ends in a /. 201 if (!preg_match('~/$~', $archivepath)) { 202 $archivepath .= '/'; 203 } 204 $expandedfiles[$archivepath] = null; 205 } else if (is_string($file)) { 206 // File specified as path on disk. 207 if (!$this->list_files_path($expandedfiles, $archivepath, $file, 208 $progress, $done)) { 209 gzclose($gz); 210 unlink($archivefile); 211 return false; 212 } 213 } else if (is_array($file)) { 214 // File specified as raw content in array. 215 $expandedfiles[$archivepath] = $file; 216 } else { 217 // File specified as stored_file object. 218 $this->list_files_stored($expandedfiles, $archivepath, $file); 219 } 220 } 221 222 // Store the list of files as a special file that is first in the 223 // archive. This contains enough information to implement list_files 224 // if required later. 225 $list = self::ARCHIVE_INDEX_COUNT_PREFIX . count($expandedfiles) . "\n"; 226 $sizes = array(); 227 $mtimes = array(); 228 foreach ($expandedfiles as $archivepath => $file) { 229 // Check archivepath doesn't contain any non-ASCII characters. 230 if (!preg_match('~^[\x00-\xff]*$~', $archivepath)) { 231 throw new coding_exception( 232 'Non-ASCII paths not supported: ' . $archivepath); 233 } 234 235 // Build up the details. 236 $type = 'f'; 237 $mtime = '?'; 238 if (is_null($file)) { 239 $type = 'd'; 240 $size = 0; 241 } else if (is_string($file)) { 242 $stat = stat($file); 243 $mtime = (int)$stat['mtime']; 244 $size = (int)$stat['size']; 245 } else if (is_array($file)) { 246 $size = (int)strlen(reset($file)); 247 } else { 248 $mtime = (int)$file->get_timemodified(); 249 $size = (int)$file->get_filesize(); 250 } 251 $sizes[$archivepath] = $size; 252 $mtimes[$archivepath] = $mtime; 253 254 // Write a line in the index. 255 $list .= "$archivepath\t$type\t$size\t$mtime\n"; 256 } 257 258 // The index file is optional; only write into archive if needed. 259 if ($this->includeindex) { 260 // Put the index file into the archive. 261 $this->write_tar_entry($gz, self::ARCHIVE_INDEX_FILE, null, strlen($list), '?', $list); 262 } 263 264 // Update progress ready for main stage. 265 $done = (int)(self::PROGRESS_MAX / 10); 266 if ($progress) { 267 $progress->progress($done, self::PROGRESS_MAX); 268 } 269 if ($expandedfiles) { 270 // The remaining 9/10ths of progress represents these files. 271 $progressperfile = (int)((9 * self::PROGRESS_MAX) / (10 * count($expandedfiles))); 272 } else { 273 $progressperfile = 1; 274 } 275 276 // Actually write entries for each file/directory. 277 foreach ($expandedfiles as $archivepath => $file) { 278 if (is_null($file)) { 279 // Null entry indicates a directory. 280 $this->write_tar_entry($gz, $archivepath, null, 281 $sizes[$archivepath], $mtimes[$archivepath]); 282 } else if (is_string($file)) { 283 // String indicates an OS file. 284 $this->write_tar_entry($gz, $archivepath, $file, 285 $sizes[$archivepath], $mtimes[$archivepath], null, $progress, $done); 286 } else if (is_array($file)) { 287 // Array indicates in-memory data. 288 $data = reset($file); 289 $this->write_tar_entry($gz, $archivepath, null, 290 $sizes[$archivepath], $mtimes[$archivepath], $data, $progress, $done); 291 } else { 292 // Stored_file object. 293 $this->write_tar_entry($gz, $archivepath, $file->get_content_file_handle(), 294 $sizes[$archivepath], $mtimes[$archivepath], null, $progress, $done); 295 } 296 $done += $progressperfile; 297 if ($progress) { 298 $progress->progress($done, self::PROGRESS_MAX); 299 } 300 } 301 302 // Finish tar file with two empty 512-byte records. 303 gzwrite($gz, str_pad('', 2 * self::TAR_BLOCK_SIZE, "\x00")); 304 gzclose($gz); 305 return true; 306 } catch (Exception $e) { 307 // If there is an exception, delete the in-progress file. 308 gzclose($gz); 309 unlink($archivefile); 310 throw $e; 311 } 312 } 313 314 /** 315 * Writes a single tar file to the archive, including its header record and 316 * then the file contents. 317 * 318 * @param resource $gz Gzip file 319 * @param string $archivepath Full path of file within archive 320 * @param string|resource $file Full path of file on disk or file handle or null if none 321 * @param int $size Size or 0 for directories 322 * @param int|string $mtime Time or ? if unknown 323 * @param string $content Actual content of file to write (null if using $filepath) 324 * @param file_progress $progress Progress indicator or null if none 325 * @param int $done Value for progress indicator 326 * @return bool True if OK 327 * @throws coding_exception If names aren't valid 328 */ 329 protected function write_tar_entry($gz, $archivepath, $file, $size, $mtime, $content = null, 330 file_progress $progress = null, $done = 0) { 331 // Header based on documentation of POSIX ustar format from: 332 // http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current . 333 334 // For directories, ensure name ends in a slash. 335 $directory = false; 336 if ($size === 0 && is_null($file)) { 337 $directory = true; 338 if (!preg_match('~/$~', $archivepath)) { 339 $archivepath .= '/'; 340 } 341 $mode = '755'; 342 } else { 343 $mode = '644'; 344 } 345 346 // Split archivepath into name and prefix. 347 $name = $archivepath; 348 $prefix = ''; 349 while (strlen($name) > 100) { 350 $slash = strpos($name, '/'); 351 if ($slash === false) { 352 throw new coding_exception( 353 'Name cannot fit length restrictions (> 100 characters): ' . $archivepath); 354 } 355 356 if ($prefix !== '') { 357 $prefix .= '/'; 358 } 359 $prefix .= substr($name, 0, $slash); 360 $name = substr($name, $slash + 1); 361 if (strlen($prefix) > 155) { 362 throw new coding_exception( 363 'Name cannot fit length restrictions (path too long): ' . $archivepath); 364 } 365 } 366 367 // Checksum performance is a bit slow because of having to call 'ord' 368 // lots of times (it takes about 1/3 the time of the actual gzwrite 369 // call). To improve performance of checksum calculation, we will 370 // store all the non-zero, non-fixed bytes that need adding to the 371 // checksum, and checksum only those bytes. 372 $forchecksum = $name; 373 374 // struct header_posix_ustar { 375 // char name[100]; 376 $header = str_pad($name, 100, "\x00"); 377 378 // char mode[8]; 379 // char uid[8]; 380 // char gid[8]; 381 $header .= '0000' . $mode . "\x000000000\x000000000\x00"; 382 $forchecksum .= $mode; 383 384 // char size[12]; 385 $octalsize = decoct($size); 386 if (strlen($octalsize) > 11) { 387 throw new coding_exception( 388 'File too large for .tar file: ' . $archivepath . ' (' . $size . ' bytes)'); 389 } 390 $paddedsize = str_pad($octalsize, 11, '0', STR_PAD_LEFT); 391 $forchecksum .= $paddedsize; 392 $header .= $paddedsize . "\x00"; 393 394 // char mtime[12]; 395 if ($mtime === '?') { 396 // Use a default timestamp rather than zero; GNU tar outputs 397 // warnings about zeroes here. 398 $mtime = self::DEFAULT_TIMESTAMP; 399 } 400 $octaltime = decoct($mtime); 401 $paddedtime = str_pad($octaltime, 11, '0', STR_PAD_LEFT); 402 $forchecksum .= $paddedtime; 403 $header .= $paddedtime . "\x00"; 404 405 // char checksum[8]; 406 // Checksum needs to be completed later. 407 $header .= ' '; 408 409 // char typeflag[1]; 410 $typeflag = $directory ? '5' : '0'; 411 $forchecksum .= $typeflag; 412 $header .= $typeflag; 413 414 // char linkname[100]; 415 $header .= str_pad('', 100, "\x00"); 416 417 // char magic[6]; 418 // char version[2]; 419 $header .= "ustar\x0000"; 420 421 // char uname[32]; 422 // char gname[32]; 423 // char devmajor[8]; 424 // char devminor[8]; 425 $header .= str_pad('', 80, "\x00"); 426 427 // char prefix[155]; 428 // char pad[12]; 429 $header .= str_pad($prefix, 167, "\x00"); 430 $forchecksum .= $prefix; 431 432 // }; 433 434 // We have now calculated the header, but without the checksum. To work 435 // out the checksum, sum all the bytes that aren't fixed or zero, and add 436 // to a standard value that contains all the fixed bytes. 437 438 // The fixed non-zero bytes are: 439 // 440 // '000000000000000000 ustar00' 441 // mode (except 3 digits), uid, gid, checksum space, magic number, version 442 // 443 // To calculate the number, call the calculate_checksum function on the 444 // above string. The result is 1775. 445 $checksum = 1775 + self::calculate_checksum($forchecksum); 446 447 $octalchecksum = str_pad(decoct($checksum), 6, '0', STR_PAD_LEFT) . "\x00 "; 448 449 // Slot it into place in the header. 450 $header = substr($header, 0, 148) . $octalchecksum . substr($header, 156); 451 452 if (strlen($header) != self::TAR_BLOCK_SIZE) { 453 throw new coding_exception('Header block wrong size!!!!!'); 454 } 455 456 // Awesome, now write out the header. 457 gzwrite($gz, $header); 458 459 // Special pre-handler for OS filename. 460 if (is_string($file)) { 461 $file = fopen($file, 'rb'); 462 if (!$file) { 463 return false; 464 } 465 } 466 467 if ($content !== null) { 468 // Write in-memory content if any. 469 if (strlen($content) !== $size) { 470 throw new coding_exception('Mismatch between provided sizes: ' . $archivepath); 471 } 472 gzwrite($gz, $content); 473 } else if ($file !== null) { 474 // Write file content if any, using a 64KB buffer. 475 $written = 0; 476 $chunks = 0; 477 while (true) { 478 $data = fread($file, 65536); 479 if ($data === false || strlen($data) == 0) { 480 break; 481 } 482 $written += gzwrite($gz, $data); 483 484 // After every megabyte of large files, update the progress 485 // tracker (so there are no long gaps without progress). 486 $chunks++; 487 if ($chunks == 16) { 488 $chunks = 0; 489 if ($progress) { 490 // This call always has the same values, but that gives 491 // the tracker a chance to indicate indeterminate 492 // progress and output something to avoid timeouts. 493 $progress->progress($done, self::PROGRESS_MAX); 494 } 495 } 496 } 497 fclose($file); 498 499 if ($written !== $size) { 500 throw new coding_exception('Mismatch between provided sizes: ' . $archivepath . 501 ' (was ' . $written . ', expected ' . $size . ')'); 502 } 503 } else if ($size != 0) { 504 throw new coding_exception('Missing data file handle for non-empty file'); 505 } 506 507 // Pad out final 512-byte block in file, if applicable. 508 $leftover = self::TAR_BLOCK_SIZE - ($size % self::TAR_BLOCK_SIZE); 509 if ($leftover == 512) { 510 $leftover = 0; 511 } else { 512 gzwrite($gz, str_pad('', $leftover, "\x00")); 513 } 514 515 return true; 516 } 517 518 /** 519 * Calculates a checksum by summing all characters of the binary string 520 * (treating them as unsigned numbers). 521 * 522 * @param string $str Input string 523 * @return int Checksum 524 */ 525 protected static function calculate_checksum($str) { 526 $checksum = 0; 527 $checklength = strlen($str); 528 for ($i = 0; $i < $checklength; $i++) { 529 $checksum += ord($str[$i]); 530 } 531 return $checksum; 532 } 533 534 /** 535 * Based on an OS path, adds either that path (if it's a file) or 536 * all its children (if it's a directory) into the list of files to 537 * archive. 538 * 539 * If a progress indicator is supplied and if this corresponds to a 540 * directory, then it will be repeatedly called with the same values. This 541 * allows the progress handler to respond in some way to avoid timeouts 542 * if required. 543 * 544 * @param array $expandedfiles List of all files to archive (output) 545 * @param string $archivepath Current path within archive 546 * @param string $path OS path on disk 547 * @param file_progress|null $progress Progress indicator or null if none 548 * @param int $done Value for progress indicator 549 * @return bool True if successful 550 */ 551 protected function list_files_path(array &$expandedfiles, $archivepath, $path, 552 ?file_progress $progress , $done) { 553 if (is_dir($path)) { 554 // Unless we're using this directory as archive root, add a 555 // directory entry. 556 if ($archivepath != '') { 557 // Add directory-creation record. 558 $expandedfiles[$archivepath . '/'] = null; 559 } 560 561 // Loop through directory contents and recurse. 562 if (!$handle = opendir($path)) { 563 return false; 564 } 565 while (false !== ($entry = readdir($handle))) { 566 if ($entry === '.' || $entry === '..') { 567 continue; 568 } 569 $result = $this->list_files_path($expandedfiles, 570 $archivepath . '/' . $entry, $path . '/' . $entry, 571 $progress, $done); 572 if (!$result) { 573 return false; 574 } 575 if ($progress) { 576 $progress->progress($done, self::PROGRESS_MAX); 577 } 578 } 579 closedir($handle); 580 } else { 581 // Just add it to list. 582 $expandedfiles[$archivepath] = $path; 583 } 584 return true; 585 } 586 587 /** 588 * Based on a stored_file objects, adds either that file (if it's a file) or 589 * all its children (if it's a directory) into the list of files to 590 * archive. 591 * 592 * If a progress indicator is supplied and if this corresponds to a 593 * directory, then it will be repeatedly called with the same values. This 594 * allows the progress handler to respond in some way to avoid timeouts 595 * if required. 596 * 597 * @param array $expandedfiles List of all files to archive (output) 598 * @param string $archivepath Current path within archive 599 * @param stored_file $file File object 600 */ 601 protected function list_files_stored(array &$expandedfiles, $archivepath, stored_file $file) { 602 if ($file->is_directory()) { 603 // Add a directory-creation record. 604 $expandedfiles[$archivepath . '/'] = null; 605 606 // Loop through directory contents (this is a recursive collection 607 // of all children not just one directory). 608 $fs = get_file_storage(); 609 $baselength = strlen($file->get_filepath()); 610 $files = $fs->get_directory_files( 611 $file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), 612 $file->get_filepath(), true, true); 613 foreach ($files as $childfile) { 614 // Get full pathname after original part. 615 $path = $childfile->get_filepath(); 616 $path = substr($path, $baselength); 617 $path = $archivepath . '/' . $path; 618 if ($childfile->is_directory()) { 619 $childfile = null; 620 } else { 621 $path .= $childfile->get_filename(); 622 } 623 $expandedfiles[$path] = $childfile; 624 } 625 } else { 626 // Just add it to list. 627 $expandedfiles[$archivepath] = $file; 628 } 629 } 630 631 /** 632 * Extract file to given file path (real OS filesystem), existing files are overwritten. 633 * 634 * @param stored_file|string $archivefile full pathname of zip file or stored_file instance 635 * @param string $pathname target directory 636 * @param array $onlyfiles only extract files present in the array 637 * @param file_progress $progress Progress indicator callback or null if not required 638 * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error 639 * details. 640 * @return array list of processed files (name=>true) 641 * @throws moodle_exception If error 642 */ 643 public function extract_to_pathname($archivefile, $pathname, 644 array $onlyfiles = null, file_progress $progress = null, $returnbool = false) { 645 $extractor = new tgz_extractor($archivefile); 646 try { 647 $result = $extractor->extract( 648 new tgz_packer_extract_to_pathname($pathname, $onlyfiles), $progress); 649 if ($returnbool) { 650 if (!is_array($result)) { 651 return false; 652 } 653 foreach ($result as $status) { 654 if ($status !== true) { 655 return false; 656 } 657 } 658 return true; 659 } else { 660 return $result; 661 } 662 } catch (moodle_exception $e) { 663 if ($returnbool) { 664 return false; 665 } else { 666 throw $e; 667 } 668 } 669 } 670 671 /** 672 * Extract file to given file path (real OS filesystem), existing files are overwritten. 673 * 674 * @param string|stored_file $archivefile full pathname of zip file or stored_file instance 675 * @param int $contextid context ID 676 * @param string $component component 677 * @param string $filearea file area 678 * @param int $itemid item ID 679 * @param string $pathbase file path 680 * @param int $userid user ID 681 * @param file_progress $progress Progress indicator callback or null if not required 682 * @return array list of processed files (name=>true) 683 * @throws moodle_exception If error 684 */ 685 public function extract_to_storage($archivefile, $contextid, 686 $component, $filearea, $itemid, $pathbase, $userid = null, 687 file_progress $progress = null) { 688 $extractor = new tgz_extractor($archivefile); 689 return $extractor->extract( 690 new tgz_packer_extract_to_storage($contextid, $component, 691 $filearea, $itemid, $pathbase, $userid), $progress); 692 } 693 694 /** 695 * Returns array of info about all files in archive. 696 * 697 * @param string|stored_file $archivefile 698 * @return array of file infos 699 */ 700 public function list_files($archivefile) { 701 $extractor = new tgz_extractor($archivefile); 702 return $extractor->list_files(); 703 } 704 705 /** 706 * Checks whether a file appears to be a .tar.gz file. 707 * 708 * @param string|stored_file $archivefile 709 * @return bool True if file contains the gzip magic number 710 */ 711 public static function is_tgz_file($archivefile) { 712 if (is_a($archivefile, 'stored_file')) { 713 $fp = $archivefile->get_content_file_handle(); 714 } else { 715 $fp = fopen($archivefile, 'rb'); 716 } 717 $firstbytes = fread($fp, 2); 718 fclose($fp); 719 return ($firstbytes[0] == "\x1f" && $firstbytes[1] == "\x8b"); 720 } 721 722 /** 723 * The zlib extension is required for this packer to work. This is a single 724 * location for the code to check whether the extension is available. 725 * 726 * @deprecated since 2.7 Always true because zlib extension is now required. 727 * 728 * @return bool True if the zlib extension is available OK 729 */ 730 public static function has_required_extension() { 731 return extension_loaded('zlib'); 732 } 733 } 734 735 736 /** 737 * Handles extraction to pathname. 738 */ 739 class tgz_packer_extract_to_pathname implements tgz_extractor_handler { 740 /** 741 * @var string Target directory for extract. 742 */ 743 protected $pathname; 744 /** 745 * @var array Array of files to extract (other files are skipped). 746 */ 747 protected $onlyfiles; 748 749 /** 750 * Constructor. 751 * 752 * @param string $pathname target directory 753 * @param array $onlyfiles only extract files present in the array 754 */ 755 public function __construct($pathname, array $onlyfiles = null) { 756 $this->pathname = $pathname; 757 $this->onlyfiles = $onlyfiles; 758 } 759 760 /** 761 * @see tgz_extractor_handler::tgz_start_file() 762 */ 763 public function tgz_start_file($archivepath) { 764 // Check file restriction. 765 if ($this->onlyfiles !== null && !in_array($archivepath, $this->onlyfiles)) { 766 return null; 767 } 768 // Ensure directory exists and prepare filename. 769 $fullpath = $this->pathname . '/' . $archivepath; 770 check_dir_exists(dirname($fullpath)); 771 return $fullpath; 772 } 773 774 /** 775 * @see tgz_extractor_handler::tgz_end_file() 776 */ 777 public function tgz_end_file($archivepath, $realpath) { 778 // Do nothing. 779 } 780 781 /** 782 * @see tgz_extractor_handler::tgz_directory() 783 */ 784 public function tgz_directory($archivepath, $mtime) { 785 // Check file restriction. 786 if ($this->onlyfiles !== null && !in_array($archivepath, $this->onlyfiles)) { 787 return false; 788 } 789 // Ensure directory exists. 790 $fullpath = $this->pathname . '/' . $archivepath; 791 check_dir_exists($fullpath); 792 return true; 793 } 794 } 795 796 797 /** 798 * Handles extraction to file storage. 799 */ 800 class tgz_packer_extract_to_storage implements tgz_extractor_handler { 801 /** 802 * @var string Path to temp file. 803 */ 804 protected $tempfile; 805 806 /** 807 * @var int Context id for files. 808 */ 809 protected $contextid; 810 /** 811 * @var string Component name for files. 812 */ 813 protected $component; 814 /** 815 * @var string File area for files. 816 */ 817 protected $filearea; 818 /** 819 * @var int Item ID for files. 820 */ 821 protected $itemid; 822 /** 823 * @var string Base path for files (subfolders will go inside this). 824 */ 825 protected $pathbase; 826 /** 827 * @var int User id for files or null if none. 828 */ 829 protected $userid; 830 831 /** 832 * Constructor. 833 * 834 * @param int $contextid Context id for files. 835 * @param string $component Component name for files. 836 * @param string $filearea File area for files. 837 * @param int $itemid Item ID for files. 838 * @param string $pathbase Base path for files (subfolders will go inside this). 839 * @param int $userid User id for files or null if none. 840 */ 841 public function __construct($contextid, $component, $filearea, $itemid, $pathbase, $userid) { 842 global $CFG; 843 844 // Store all data. 845 $this->contextid = $contextid; 846 $this->component = $component; 847 $this->filearea = $filearea; 848 $this->itemid = $itemid; 849 $this->pathbase = $pathbase; 850 $this->userid = $userid; 851 852 // Obtain temp filename. 853 $tempfolder = $CFG->tempdir . '/core_files'; 854 check_dir_exists($tempfolder); 855 $this->tempfile = tempnam($tempfolder, '.dat'); 856 } 857 858 /** 859 * @see tgz_extractor_handler::tgz_start_file() 860 */ 861 public function tgz_start_file($archivepath) { 862 // All files are stored in the same filename. 863 return $this->tempfile; 864 } 865 866 /** 867 * @see tgz_extractor_handler::tgz_end_file() 868 */ 869 public function tgz_end_file($archivepath, $realpath) { 870 // Place temp file into storage. 871 $fs = get_file_storage(); 872 $filerecord = array('contextid' => $this->contextid, 'component' => $this->component, 873 'filearea' => $this->filearea, 'itemid' => $this->itemid); 874 $filerecord['filepath'] = $this->pathbase . dirname($archivepath) . '/'; 875 $filerecord['filename'] = basename($archivepath); 876 if ($this->userid) { 877 $filerecord['userid'] = $this->userid; 878 } 879 // Delete existing file (if any) and create new one. 880 tgz_packer::delete_existing_file_record($fs, $filerecord); 881 $fs->create_file_from_pathname($filerecord, $this->tempfile); 882 unlink($this->tempfile); 883 } 884 885 /** 886 * @see tgz_extractor_handler::tgz_directory() 887 */ 888 public function tgz_directory($archivepath, $mtime) { 889 // Standardise path. 890 if (!preg_match('~/$~', $archivepath)) { 891 $archivepath .= '/'; 892 } 893 // Create directory if it doesn't already exist. 894 $fs = get_file_storage(); 895 if (!$fs->file_exists($this->contextid, $this->component, $this->filearea, $this->itemid, 896 $this->pathbase . $archivepath, '.')) { 897 $fs->create_directory($this->contextid, $this->component, $this->filearea, $this->itemid, 898 $this->pathbase . $archivepath); 899 } 900 return true; 901 } 902 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body