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  namespace core;
  18  
  19  use file_progress;
  20  use tgz_packer;
  21  
  22  defined('MOODLE_INTERNAL') || die();
  23  
  24  global $CFG;
  25  require_once($CFG->libdir . '/filestorage/file_progress.php');
  26  
  27  /**
  28   * Unit tests for /lib/filestorage/tgz_packer.php and tgz_extractor.php.
  29   *
  30   * @package core
  31   * @copyright 2013 The Open University
  32   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class tgz_packer_test extends \advanced_testcase implements file_progress {
  35      /**
  36       * @var array Progress information passed to the progress reporter
  37       */
  38      protected $progress;
  39  
  40      /**
  41       * Puts contents with specified time.
  42       *
  43       * @param string $path File path
  44       * @param string $contents Contents of file
  45       * @param int $mtime Time modified
  46       */
  47      protected static function file_put_contents_at_time($path, $contents, $mtime) {
  48          file_put_contents($path, $contents);
  49          touch($path, $mtime);
  50      }
  51  
  52      /**
  53       * Set up some files to be archived.
  54       *
  55       * @return array Array listing files of all types
  56       */
  57      protected function prepare_file_list() {
  58          global $CFG;
  59          $this->resetAfterTest(true);
  60  
  61          // Make array listing files to archive.
  62          $filelist = array();
  63  
  64          // Normal file.
  65          self::file_put_contents_at_time($CFG->tempdir . '/file1.txt', 'File 1', 1377993601);
  66          $filelist['out1.txt'] = $CFG->tempdir . '/file1.txt';
  67  
  68          // Recursive directory w/ file and directory with file.
  69          check_dir_exists($CFG->tempdir . '/dir1/dir2');
  70          self::file_put_contents_at_time($CFG->tempdir . '/dir1/file2.txt', 'File 2', 1377993602);
  71          self::file_put_contents_at_time($CFG->tempdir . '/dir1/dir2/file3.txt', 'File 3', 1377993603);
  72          $filelist['out2'] = $CFG->tempdir . '/dir1';
  73  
  74          // Moodle stored_file.
  75          $context = \context_system::instance();
  76          $filerecord = array('contextid' => $context->id, 'component' => 'phpunit',
  77                  'filearea' => 'data', 'itemid' => 0, 'filepath' => '/',
  78                  'filename' => 'file4.txt', 'timemodified' => 1377993604);
  79          $fs = get_file_storage();
  80          $sf = $fs->create_file_from_string($filerecord, 'File 4');
  81          $filelist['out3.txt'] = $sf;
  82  
  83           // Moodle stored_file directory.
  84          $filerecord['itemid'] = 1;
  85          $filerecord['filepath'] = '/dir1/';
  86          $filerecord['filename'] = 'file5.txt';
  87          $filerecord['timemodified'] = 1377993605;
  88          $fs->create_file_from_string($filerecord, 'File 5');
  89          $filerecord['filepath'] = '/dir1/dir2/';
  90          $filerecord['filename'] = 'file6.txt';
  91          $filerecord['timemodified'] = 1377993606;
  92          $fs->create_file_from_string($filerecord, 'File 6');
  93          $filerecord['filepath'] = '/';
  94          $filerecord['filename'] = 'excluded.txt';
  95          $fs->create_file_from_string($filerecord, 'Excluded');
  96          $filelist['out4'] = $fs->get_file($context->id, 'phpunit', 'data', 1, '/dir1/', '.');
  97  
  98          // File stored as raw content.
  99          $filelist['out5.txt'] = array('File 7');
 100  
 101          // File where there's just an empty directory.
 102          $filelist['out6'] = null;
 103  
 104          return $filelist;
 105      }
 106  
 107      /**
 108       * Tests getting the item.
 109       */
 110      public function test_get_packer() {
 111          $packer = get_file_packer('application/x-gzip');
 112          $this->assertInstanceOf('tgz_packer', $packer);
 113      }
 114  
 115      /**
 116       * Tests basic archive and extract to file paths.
 117       */
 118      public function test_to_normal_files() {
 119          global $CFG;
 120          $packer = get_file_packer('application/x-gzip');
 121  
 122          // Archive files.
 123          $files = $this->prepare_file_list();
 124          $archivefile = $CFG->tempdir . '/test.tar.gz';
 125          $packer->archive_to_pathname($files, $archivefile);
 126  
 127          // Extract same files.
 128          $outdir = $CFG->tempdir . '/out';
 129          check_dir_exists($outdir);
 130          $result = $packer->extract_to_pathname($archivefile, $outdir);
 131  
 132          // The result array should have file entries + directory entries for
 133          // all implicit directories + entry for the explicit directory.
 134          $expectedpaths = array('out1.txt', 'out2/', 'out2/dir2/', 'out2/dir2/file3.txt',
 135                  'out2/file2.txt', 'out3.txt', 'out4/', 'out4/dir2/', 'out4/file5.txt',
 136                  'out4/dir2/file6.txt', 'out5.txt', 'out6/');
 137          sort($expectedpaths);
 138          $actualpaths = array_keys($result);
 139          sort($actualpaths);
 140          $this->assertEquals($expectedpaths, $actualpaths);
 141          foreach ($result as $path => $booleantrue) {
 142              $this->assertTrue($booleantrue);
 143          }
 144  
 145          // Check the files are as expected.
 146          $this->assertEquals('File 1', file_get_contents($outdir . '/out1.txt'));
 147          $this->assertEquals('File 2', file_get_contents($outdir . '/out2/file2.txt'));
 148          $this->assertEquals('File 3', file_get_contents($outdir . '/out2/dir2/file3.txt'));
 149          $this->assertEquals('File 4', file_get_contents($outdir . '/out3.txt'));
 150          $this->assertEquals('File 5', file_get_contents($outdir . '/out4/file5.txt'));
 151          $this->assertEquals('File 6', file_get_contents($outdir . '/out4/dir2/file6.txt'));
 152          $this->assertEquals('File 7', file_get_contents($outdir . '/out5.txt'));
 153          $this->assertTrue(is_dir($outdir . '/out6'));
 154      }
 155  
 156      /**
 157       * Tests archive and extract to Moodle file system.
 158       */
 159      public function test_to_stored_files() {
 160          global $CFG;
 161          $packer = get_file_packer('application/x-gzip');
 162  
 163          // Archive files.
 164          $files = $this->prepare_file_list();
 165          $archivefile = $CFG->tempdir . '/test.tar.gz';
 166          $context = \context_system::instance();
 167          $sf = $packer->archive_to_storage($files,
 168                  $context->id, 'phpunit', 'archive', 1, '/', 'archive.tar.gz');
 169          $this->assertInstanceOf('stored_file', $sf);
 170  
 171          // Extract (from storage) to disk.
 172          $outdir = $CFG->tempdir . '/out';
 173          check_dir_exists($outdir);
 174          $packer->extract_to_pathname($sf, $outdir);
 175  
 176          // Check the files are as expected.
 177          $this->assertEquals('File 1', file_get_contents($outdir . '/out1.txt'));
 178          $this->assertEquals('File 2', file_get_contents($outdir . '/out2/file2.txt'));
 179          $this->assertEquals('File 3', file_get_contents($outdir . '/out2/dir2/file3.txt'));
 180          $this->assertEquals('File 4', file_get_contents($outdir . '/out3.txt'));
 181          $this->assertEquals('File 5', file_get_contents($outdir . '/out4/file5.txt'));
 182          $this->assertEquals('File 6', file_get_contents($outdir . '/out4/dir2/file6.txt'));
 183          $this->assertEquals('File 7', file_get_contents($outdir . '/out5.txt'));
 184          $this->assertTrue(is_dir($outdir . '/out6'));
 185  
 186          // Extract to Moodle storage.
 187          $packer->extract_to_storage($sf, $context->id, 'phpunit', 'data', 2, '/out/');
 188          $fs = get_file_storage();
 189          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/', 'out1.txt');
 190          $this->assertNotEmpty($out);
 191          $this->assertEquals('File 1', $out->get_content());
 192          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/out2/', 'file2.txt');
 193          $this->assertNotEmpty($out);
 194          $this->assertEquals('File 2', $out->get_content());
 195          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/out2/dir2/', 'file3.txt');
 196          $this->assertNotEmpty($out);
 197          $this->assertEquals('File 3', $out->get_content());
 198          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/', 'out3.txt');
 199          $this->assertNotEmpty($out);
 200          $this->assertEquals('File 4', $out->get_content());
 201          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/out4/', 'file5.txt');
 202          $this->assertNotEmpty($out);
 203          $this->assertEquals('File 5', $out->get_content());
 204          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/out4/dir2/', 'file6.txt');
 205          $this->assertNotEmpty($out);
 206          $this->assertEquals('File 6', $out->get_content());
 207          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/', 'out5.txt');
 208          $this->assertNotEmpty($out);
 209          $this->assertEquals('File 7', $out->get_content());
 210          $out = $fs->get_file($context->id, 'phpunit', 'data', 2, '/out/out6/', '.');
 211          $this->assertNotEmpty($out);
 212          $this->assertTrue($out->is_directory());
 213  
 214          // These functions are supposed to overwrite existing files; test they
 215          // don't give errors when run twice.
 216          $sf = $packer->archive_to_storage($files,
 217                  $context->id, 'phpunit', 'archive', 1, '/', 'archive.tar.gz');
 218          $this->assertInstanceOf('stored_file', $sf);
 219          $packer->extract_to_storage($sf, $context->id, 'phpunit', 'data', 2, '/out/');
 220      }
 221  
 222      /**
 223       * Tests extracting with a list of specified files.
 224       */
 225      public function test_only_specified_files() {
 226          global $CFG;
 227          $packer = get_file_packer('application/x-gzip');
 228  
 229          // Archive files.
 230          $files = $this->prepare_file_list();
 231          $archivefile = $CFG->tempdir . '/test.tar.gz';
 232          $packer->archive_to_pathname($files, $archivefile);
 233  
 234          // Extract same files.
 235          $outdir = $CFG->tempdir . '/out';
 236          check_dir_exists($outdir);
 237          $result = $packer->extract_to_pathname($archivefile, $outdir,
 238                  array('out3.txt', 'out6/', 'out4/file5.txt'));
 239  
 240          // Check result reporting only includes specified files.
 241          $expectedpaths = array('out3.txt', 'out4/file5.txt', 'out6/');
 242          sort($expectedpaths);
 243          $actualpaths = array_keys($result);
 244          sort($actualpaths);
 245          $this->assertEquals($expectedpaths, $actualpaths);
 246  
 247          // Check the files are as expected.
 248          $this->assertFalse(file_exists($outdir . '/out1.txt'));
 249          $this->assertEquals('File 4', file_get_contents($outdir . '/out3.txt'));
 250          $this->assertEquals('File 5', file_get_contents($outdir . '/out4/file5.txt'));
 251          $this->assertTrue(is_dir($outdir . '/out6'));
 252      }
 253  
 254      /**
 255       * Tests extracting files returning only a boolean state with success.
 256       */
 257      public function test_extract_to_pathname_returnvalue_successful() {
 258          $packer = get_file_packer('application/x-gzip');
 259  
 260          // Prepare files.
 261          $files = $this->prepare_file_list();
 262          $archivefile = make_request_directory() . '/test.tgz';
 263          $packer->archive_to_pathname($files, $archivefile);
 264  
 265          // Extract same files.
 266          $outdir = make_request_directory();
 267          $result = $packer->extract_to_pathname($archivefile, $outdir, null, null, true);
 268  
 269          $this->assertTrue($result);
 270      }
 271  
 272      /**
 273       * Tests extracting files returning only a boolean state with failure.
 274       */
 275      public function test_extract_to_pathname_returnvalue_failure() {
 276          $packer = get_file_packer('application/x-gzip');
 277  
 278          // Create sample files.
 279          $archivefile = make_request_directory() . '/test.tgz';
 280          file_put_contents($archivefile, '');
 281  
 282          // Extract same files.
 283          $outdir = make_request_directory();
 284  
 285          $result = $packer->extract_to_pathname($archivefile, $outdir, null, null, true);
 286  
 287          $this->assertFalse($result);
 288      }
 289  
 290      /**
 291       * Tests the progress reporting.
 292       */
 293      public function test_file_progress() {
 294          global $CFG;
 295  
 296          // Set up.
 297          $filelist = $this->prepare_file_list();
 298          $packer = get_file_packer('application/x-gzip');
 299          $archive = "$CFG->tempdir/archive.tgz";
 300          $context = \context_system::instance();
 301  
 302          // Archive to pathname.
 303          $this->progress = array();
 304          $result = $packer->archive_to_pathname($filelist, $archive, true, $this);
 305          $this->assertTrue($result);
 306          // Should send progress at least once per file.
 307          $this->assertTrue(count($this->progress) >= count($filelist));
 308          // Progress should obey some restrictions.
 309          $this->check_progress_toward_max();
 310  
 311          // Archive to storage.
 312          $this->progress = array();
 313          $archivefile = $packer->archive_to_storage($filelist, $context->id,
 314                  'phpunit', 'test', 0, '/', 'archive.tgz', null, true, $this);
 315          $this->assertInstanceOf('stored_file', $archivefile);
 316          $this->assertTrue(count($this->progress) >= count($filelist));
 317          $this->check_progress_toward_max();
 318  
 319          // Extract to pathname.
 320          $this->progress = array();
 321          $target = "$CFG->tempdir/test/";
 322          check_dir_exists($target);
 323          $result = $packer->extract_to_pathname($archive, $target, null, $this);
 324          remove_dir($target);
 325          // We only output progress once per block, and this is kind of a small file.
 326          $this->assertTrue(count($this->progress) >= 1);
 327          $this->check_progress_toward_max();
 328  
 329          // Extract to storage (from storage).
 330          $this->progress = array();
 331          $result = $packer->extract_to_storage($archivefile, $context->id,
 332                  'phpunit', 'target', 0, '/', null, $this);
 333          $this->assertTrue(count($this->progress) >= 1);
 334          $this->check_progress_toward_max();
 335  
 336          // Extract to storage (from path).
 337          $this->progress = array();
 338          $result = $packer->extract_to_storage($archive, $context->id,
 339                  'phpunit', 'target', 0, '/', null, $this);
 340          $this->assertTrue(count($this->progress) >= 1);
 341          $this->check_progress_toward_max();
 342  
 343          // Wipe created disk file.
 344          unlink($archive);
 345      }
 346  
 347      /**
 348       * Tests the list_files function with and without an index file.
 349       */
 350      public function test_list_files() {
 351          global $CFG;
 352  
 353          // Set up.
 354          $filelist = $this->prepare_file_list();
 355          $packer = get_file_packer('application/x-gzip');
 356          $archive = "$CFG->tempdir/archive.tgz";
 357  
 358          // Archive with an index (default).
 359          $packer = get_file_packer('application/x-gzip');
 360          $result = $packer->archive_to_pathname($filelist, $archive, true, $this);
 361          $this->assertTrue($result);
 362          $hashwith = \file_storage::hash_from_path($archive);
 363  
 364          // List files.
 365          $files = $packer->list_files($archive);
 366  
 367          // Check they match expected.
 368          $expectedinfo = array(
 369              array('out1.txt', 1377993601, false, 6),
 370              array('out2/', tgz_packer::DEFAULT_TIMESTAMP, true, 0),
 371              array('out2/dir2/', tgz_packer::DEFAULT_TIMESTAMP, true, 0),
 372              array('out2/dir2/file3.txt', 1377993603, false, 6),
 373              array('out2/file2.txt', 1377993602, false, 6),
 374              array('out3.txt', 1377993604, false, 6),
 375              array('out4/', tgz_packer::DEFAULT_TIMESTAMP, true, 0),
 376              array('out4/dir2/', tgz_packer::DEFAULT_TIMESTAMP, true, 0),
 377              array('out4/dir2/file6.txt', 1377993606, false, 6),
 378              array('out4/file5.txt', 1377993605, false, 6),
 379              array('out5.txt', tgz_packer::DEFAULT_TIMESTAMP, false, 6),
 380              array('out6/', tgz_packer::DEFAULT_TIMESTAMP, true, 0),
 381          );
 382          $this->assertEquals($expectedinfo, self::convert_info_for_assert($files));
 383  
 384          // Archive with no index. Should have same result.
 385          $this->progress = array();
 386          $packer->set_include_index(false);
 387          $result = $packer->archive_to_pathname($filelist, $archive, true, $this);
 388          $this->assertTrue($result);
 389          $hashwithout = \file_storage::hash_from_path($archive);
 390          $files = $packer->list_files($archive);
 391          $this->assertEquals($expectedinfo, self::convert_info_for_assert($files));
 392  
 393          // Check it actually is different (does have index in)!
 394          $this->assertNotEquals($hashwith, $hashwithout);
 395  
 396          // Put the index back on in case of future tests.
 397          $packer->set_include_index(true);
 398      }
 399  
 400      /**
 401       * Utility function to convert the file info array into a simpler format
 402       * for making comparisons.
 403       *
 404       * @param array $files Array from list_files result
 405       */
 406      protected static function convert_info_for_assert(array $files) {
 407          $actualinfo = array();
 408          foreach ($files as $file) {
 409              $actualinfo[] = array($file->pathname, $file->mtime, $file->is_directory, $file->size);
 410          }
 411          usort($actualinfo, function($a, $b) {
 412              return strcmp($a[0], $b[0]);
 413          });
 414          return $actualinfo;
 415      }
 416  
 417      public function test_is_tgz_file() {
 418          global $CFG;
 419  
 420          // Set up.
 421          $filelist = $this->prepare_file_list();
 422          $packer1 = get_file_packer('application/x-gzip');
 423          $packer2 = get_file_packer('application/zip');
 424          $archive2 = "$CFG->tempdir/archive.zip";
 425  
 426          // Archive in tgz and zip format.
 427          $context = \context_system::instance();
 428          $archive1 = $packer1->archive_to_storage($filelist, $context->id,
 429                  'phpunit', 'test', 0, '/', 'archive.tgz', null, true, $this);
 430          $this->assertInstanceOf('stored_file', $archive1);
 431          $result = $packer2->archive_to_pathname($filelist, $archive2);
 432          $this->assertTrue($result);
 433  
 434          // Use is_tgz_file to detect which is which. First check is from storage,
 435          // second check is from filesystem.
 436          $this->assertTrue(tgz_packer::is_tgz_file($archive1));
 437          $this->assertFalse(tgz_packer::is_tgz_file($archive2));
 438      }
 439  
 440      /**
 441       * Checks that progress reported is numeric rather than indeterminate,
 442       * and follows the progress reporting rules.
 443       */
 444      protected function check_progress_toward_max() {
 445          $lastvalue = -1; $lastmax = -1;
 446          foreach ($this->progress as $progressitem) {
 447              list($value, $max) = $progressitem;
 448              if ($lastmax != -1) {
 449                  $this->assertEquals($max, $lastmax);
 450              } else {
 451                  $lastmax = $max;
 452              }
 453              $this->assertTrue(is_integer($value));
 454              $this->assertTrue(is_integer($max));
 455              $this->assertNotEquals(file_progress::INDETERMINATE, $max);
 456              $this->assertTrue($value <= $max);
 457              $this->assertTrue($value >= $lastvalue);
 458              $lastvalue = $value;
 459          }
 460      }
 461  
 462      /**
 463       * Handles file_progress interface.
 464       *
 465       * @param int $progress
 466       * @param int $max
 467       */
 468      public function progress($progress = file_progress::INDETERMINATE, $max = file_progress::INDETERMINATE) {
 469          $this->progress[] = array($progress, $max);
 470      }
 471  }