Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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  }