Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Unit tests for file_system.
  19   *
  20   * @package   core_files
  21   * @category  phpunit
  22   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  require_once($CFG->libdir . '/filestorage/file_system.php');
  30  
  31  /**
  32   * Unit tests for file_system.
  33   *
  34   * @package   core_files
  35   * @category  phpunit
  36   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   * @coversDefaultClass \file_system
  39   */
  40  class core_files_file_system_testcase extends advanced_testcase {
  41  
  42      public function setUp(): void {
  43          get_file_storage(true);
  44      }
  45  
  46      public function tearDown(): void {
  47          get_file_storage(true);
  48      }
  49  
  50      /**
  51       * Helper function to help setup and configure the virtual file system stream.
  52       *
  53       * @param   array $filedir Directory structure and content of the filedir
  54       * @param   array $trashdir Directory structure and content of the sourcedir
  55       * @param   array $sourcedir Directory structure and content of a directory used for source files for tests
  56       * @return  \org\bovigo\vfs\vfsStream
  57       */
  58      protected function setup_vfile_root($content = []) {
  59          $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
  60  
  61          return $vfileroot;
  62      }
  63  
  64      /**
  65       * Helper to create a stored file objectw with the given supplied content.
  66       *
  67       * @param   string  $filecontent The content of the mocked file
  68       * @param   string  $filename The file name to use in the stored_file
  69       * @param   array   $mockedmethods A list of methods you intend to override
  70       *                  If no methods are specified, only abstract functions are mocked.
  71       * @return stored_file
  72       */
  73      protected function get_stored_file($filecontent, $filename = null, $mockedmethods = null) {
  74          $contenthash = file_storage::hash_from_string($filecontent);
  75          if (empty($filename)) {
  76              $filename = $contenthash;
  77          }
  78  
  79          $file = $this->getMockBuilder(stored_file::class)
  80              ->setMethods($mockedmethods)
  81              ->setConstructorArgs([
  82                  get_file_storage(),
  83                  (object) [
  84                      'contenthash' => $contenthash,
  85                      'filesize' => strlen($filecontent),
  86                      'filename' => $filename,
  87                  ]
  88              ])
  89              ->getMock();
  90  
  91          return $file;
  92      }
  93  
  94      /**
  95       * Get a testable mock of the abstract file_system class.
  96       *
  97       * @param   array   $mockedmethods A list of methods you intend to override
  98       *                  If no methods are specified, only abstract functions are mocked.
  99       * @return file_system
 100       */
 101      protected function get_testable_mock($mockedmethods = []) {
 102          $fs = $this->getMockBuilder(file_system::class)
 103              ->setMethods($mockedmethods)
 104              ->getMockForAbstractClass();
 105  
 106          return $fs;
 107      }
 108  
 109      /**
 110       * Ensure that the file system is not clonable.
 111       *
 112       */
 113      public function test_not_cloneable() {
 114          $reflection = new ReflectionClass('file_system');
 115          $this->assertFalse($reflection->isCloneable());
 116      }
 117  
 118      /**
 119       * Ensure that the filedir file_system extension is used by default.
 120       *
 121       */
 122      public function test_default_class() {
 123          $this->resetAfterTest();
 124  
 125          // Ensure that the alternative_file_system_class is null.
 126          global $CFG;
 127          $CFG->alternative_file_system_class = null;
 128  
 129          $storage = get_file_storage();
 130          $fs = $storage->get_file_system();
 131          $this->assertInstanceOf(file_system::class, $fs);
 132          $this->assertEquals(file_system_filedir::class, get_class($fs));
 133      }
 134  
 135      /**
 136       * Ensure that the specified file_system extension class is used.
 137       *
 138       */
 139      public function test_supplied_class() {
 140          global $CFG;
 141          $this->resetAfterTest();
 142  
 143          // Mock the file_system.
 144          // Mocks create a new child of the mocked class which is perfect for this test.
 145          $filesystem = $this->getMockBuilder('file_system')
 146              ->disableOriginalConstructor()
 147              ->getMock();
 148          $CFG->alternative_file_system_class = get_class($filesystem);
 149  
 150          $storage = get_file_storage();
 151          $fs = $storage->get_file_system();
 152          $this->assertInstanceOf(file_system::class, $fs);
 153          $this->assertEquals(get_class($filesystem), get_class($fs));
 154      }
 155  
 156      /**
 157       * Test that the readfile function outputs content to disk.
 158       *
 159       * @covers ::readfile
 160       */
 161      public function test_readfile_remote() {
 162          global $CFG;
 163  
 164          // Mock the filesystem.
 165          $filecontent = 'example content';
 166          $vfileroot = $this->setup_vfile_root(['sourcefile' => $filecontent]);
 167          $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcefile');
 168  
 169          $file = $this->get_stored_file($filecontent);
 170  
 171          // Mock the file_system class.
 172          // We need to override the get_remote_path_from_storedfile function.
 173          $fs = $this->get_testable_mock([
 174              'get_remote_path_from_storedfile',
 175              'is_file_readable_locally_by_storedfile',
 176              'get_local_path_from_storedfile',
 177          ]);
 178          $fs->method('get_remote_path_from_storedfile')->willReturn($filepath);
 179          $fs->method('is_file_readable_locally_by_storedfile')->willReturn(false);
 180          $fs->expects($this->never())->method('get_local_path_from_storedfile');
 181  
 182          // Note: It is currently not possible to mock readfile_allow_large
 183          // because file_system is in the global namespace.
 184          // We must therefore check for expected output. This is not ideal.
 185          $this->expectOutputString($filecontent);
 186          $fs->readfile($file);
 187      }
 188  
 189      /**
 190       * Test that the readfile function outputs content to disk.
 191       *
 192       * @covers ::readfile
 193       */
 194      public function test_readfile_local() {
 195          global $CFG;
 196  
 197          // Mock the filesystem.
 198          $filecontent = 'example content';
 199          $vfileroot = $this->setup_vfile_root(['sourcefile' => $filecontent]);
 200          $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcefile');
 201  
 202          $file = $this->get_stored_file($filecontent);
 203  
 204          // Mock the file_system class.
 205          // We need to override the get_remote_path_from_storedfile function.
 206          $fs = $this->get_testable_mock([
 207              'get_remote_path_from_storedfile',
 208              'is_file_readable_locally_by_storedfile',
 209              'get_local_path_from_storedfile',
 210          ]);
 211          $fs->method('is_file_readable_locally_by_storedfile')->willReturn(true);
 212          $fs->expects($this->never())->method('get_remote_path_from_storedfile');
 213          $fs->expects($this->once())->method('get_local_path_from_storedfile')->willReturn($filepath);
 214  
 215          // Note: It is currently not possible to mock readfile_allow_large
 216          // because file_system is in the global namespace.
 217          // We must therefore check for expected output. This is not ideal.
 218          $this->expectOutputString($filecontent);
 219          $fs->readfile($file);
 220      }
 221  
 222      /**
 223       * Test that the get_local_path_from_storedfile function functions
 224       * correctly when called with various args.
 225       *
 226       * @dataProvider get_local_path_from_storedfile_provider
 227       * @param   array   $args The additional args to pass to get_local_path_from_storedfile
 228       * @param   bool    $fetch Whether the combination of args should have caused a fetch
 229       *
 230       * @covers ::get_local_path_from_storedfile
 231       */
 232      public function test_get_local_path_from_storedfile($args, $fetch) {
 233          $filepath = '/path/to/file';
 234          $filecontent = 'example content';
 235  
 236          // Get the filesystem mock.
 237          $fs = $this->get_testable_mock([
 238              'get_local_path_from_hash',
 239          ]);
 240          $fs->expects($this->once())
 241              ->method('get_local_path_from_hash')
 242              ->with($this->equalTo(file_storage::hash_from_string($filecontent)), $this->equalTo($fetch))
 243              ->willReturn($filepath);
 244  
 245          $file = $this->get_stored_file($filecontent);
 246  
 247          $result = $fs->get_local_path_from_storedfile($file, $fetch);
 248  
 249          $this->assertEquals($filepath, $result);
 250      }
 251  
 252      /**
 253       * Ensure that the default implementation of get_remote_path_from_storedfile
 254       * simply calls get_local_path_from_storedfile without requiring a
 255       * fetch.
 256       *
 257       * @covers ::get_remote_path_from_storedfile
 258       */
 259      public function test_get_remote_path_from_storedfile() {
 260          $filepath = '/path/to/file';
 261          $filecontent = 'example content';
 262  
 263          $fs = $this->get_testable_mock([
 264              'get_remote_path_from_hash',
 265          ]);
 266  
 267          $fs->expects($this->once())
 268              ->method('get_remote_path_from_hash')
 269              ->with($this->equalTo(file_storage::hash_from_string($filecontent)), $this->equalTo(false))
 270              ->willReturn($filepath);
 271  
 272          $file = $this->get_stored_file($filecontent);
 273  
 274          $result = $fs->get_remote_path_from_storedfile($file);
 275  
 276          $this->assertEquals($filepath, $result);
 277      }
 278  
 279      /**
 280       * Test the stock implementation of is_file_readable_locally_by_hash with a valid file.
 281       *
 282       * This should call get_local_path_from_hash and check the readability
 283       * of the file.
 284       *
 285       * Fetching the file is optional.
 286       *
 287       * @covers ::is_file_readable_locally_by_hash
 288       */
 289      public function test_is_file_readable_locally_by_hash() {
 290          $filecontent = 'example content';
 291          $contenthash = file_storage::hash_from_string($filecontent);
 292          $filepath = __FILE__;
 293  
 294          $fs = $this->get_testable_mock([
 295              'get_local_path_from_hash',
 296          ]);
 297  
 298          $fs->method('get_local_path_from_hash')
 299              ->with($this->equalTo($contenthash), $this->equalTo(false))
 300              ->willReturn($filepath);
 301  
 302          $this->assertTrue($fs->is_file_readable_locally_by_hash($contenthash));
 303      }
 304  
 305      /**
 306       * Test the stock implementation of is_file_readable_locally_by_hash with an empty file.
 307       *
 308       * @covers ::is_file_readable_locally_by_hash
 309       */
 310      public function test_is_file_readable_locally_by_hash_empty() {
 311          $filecontent = '';
 312          $contenthash = file_storage::hash_from_string($filecontent);
 313  
 314          $fs = $this->get_testable_mock([
 315              'get_local_path_from_hash',
 316          ]);
 317  
 318          $fs->expects($this->never())
 319              ->method('get_local_path_from_hash');
 320  
 321          $this->assertTrue($fs->is_file_readable_locally_by_hash($contenthash));
 322      }
 323  
 324      /**
 325       * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
 326       *
 327       * @covers ::is_file_readable_remotely_by_hash
 328       */
 329      public function test_is_file_readable_remotely_by_hash() {
 330          $filecontent = 'example content';
 331          $contenthash = file_storage::hash_from_string($filecontent);
 332  
 333          $fs = $this->get_testable_mock([
 334              'get_remote_path_from_hash',
 335          ]);
 336  
 337          $fs->method('get_remote_path_from_hash')
 338              ->with($this->equalTo($contenthash), $this->equalTo(false))
 339              ->willReturn(__FILE__);
 340  
 341          $this->assertTrue($fs->is_file_readable_remotely_by_hash($contenthash));
 342      }
 343  
 344      /**
 345       * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
 346       *
 347       * @covers ::is_file_readable_remotely_by_hash
 348       */
 349      public function test_is_file_readable_remotely_by_hash_empty() {
 350          $filecontent = '';
 351          $contenthash = file_storage::hash_from_string($filecontent);
 352  
 353          $fs = $this->get_testable_mock([
 354              'get_remote_path_from_hash',
 355          ]);
 356  
 357          $fs->expects($this->never())
 358              ->method('get_remote_path_from_hash');
 359  
 360          $this->assertTrue($fs->is_file_readable_remotely_by_hash($contenthash));
 361      }
 362  
 363      /**
 364       * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
 365       *
 366       * @covers ::is_file_readable_remotely_by_hash
 367       */
 368      public function test_is_file_readable_remotely_by_hash_not_found() {
 369          $filecontent = 'example content';
 370          $contenthash = file_storage::hash_from_string($filecontent);
 371  
 372          $fs = $this->get_testable_mock([
 373              'get_remote_path_from_hash',
 374          ]);
 375  
 376          $fs->method('get_remote_path_from_hash')
 377              ->with($this->equalTo($contenthash), $this->equalTo(false))
 378              ->willReturn('/path/to/nonexistent/file');
 379  
 380          $this->assertFalse($fs->is_file_readable_remotely_by_hash($contenthash));
 381      }
 382  
 383      /**
 384       * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
 385       *
 386       * @covers ::is_file_readable_remotely_by_storedfile
 387       */
 388      public function test_is_file_readable_remotely_by_storedfile() {
 389          $file = $this->get_stored_file('example content');
 390  
 391          $fs = $this->get_testable_mock([
 392              'get_remote_path_from_storedfile',
 393          ]);
 394  
 395          $fs->method('get_remote_path_from_storedfile')
 396              ->willReturn(__FILE__);
 397  
 398          $this->assertTrue($fs->is_file_readable_remotely_by_storedfile($file));
 399      }
 400  
 401      /**
 402       * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
 403       *
 404       * @covers ::is_file_readable_remotely_by_storedfile
 405       */
 406      public function test_is_file_readable_remotely_by_storedfile_empty() {
 407          $fs = $this->get_testable_mock([
 408              'get_remote_path_from_storedfile',
 409          ]);
 410  
 411          $fs->expects($this->never())
 412              ->method('get_remote_path_from_storedfile');
 413  
 414          $file = $this->get_stored_file('');
 415          $this->assertTrue($fs->is_file_readable_remotely_by_storedfile($file));
 416      }
 417  
 418      /**
 419       * Test the stock implementation of is_file_readable_locally_by_storedfile with an empty file.
 420       *
 421       * @covers ::is_file_readable_locally_by_storedfile
 422       */
 423      public function test_is_file_readable_locally_by_storedfile_empty() {
 424          $fs = $this->get_testable_mock([
 425              'get_local_path_from_storedfile',
 426          ]);
 427  
 428          $fs->expects($this->never())
 429              ->method('get_local_path_from_storedfile');
 430  
 431          $file = $this->get_stored_file('');
 432          $this->assertTrue($fs->is_file_readable_locally_by_storedfile($file));
 433      }
 434  
 435      /**
 436       * Test the stock implementation of is_file_readable_remotely_by_storedfile with a valid file.
 437       *
 438       * @covers ::is_file_readable_locally_by_storedfile
 439       */
 440      public function test_is_file_readable_remotely_by_storedfile_not_found() {
 441          $file = $this->get_stored_file('example content');
 442  
 443          $fs = $this->get_testable_mock([
 444              'get_remote_path_from_storedfile',
 445          ]);
 446  
 447          $fs->method('get_remote_path_from_storedfile')
 448              ->willReturn(__LINE__);
 449  
 450          $this->assertFalse($fs->is_file_readable_remotely_by_storedfile($file));
 451      }
 452  
 453      /**
 454       * Test the stock implementation of is_file_readable_locally_by_storedfile with a valid file.
 455       *
 456       * @covers ::is_file_readable_locally_by_storedfile
 457       */
 458      public function test_is_file_readable_locally_by_storedfile_unreadable() {
 459          $fs = $this->get_testable_mock([
 460              'get_local_path_from_storedfile',
 461          ]);
 462          $file = $this->get_stored_file('example content');
 463  
 464          $fs->method('get_local_path_from_storedfile')
 465              ->with($this->equalTo($file), $this->equalTo(false))
 466              ->willReturn('/path/to/nonexistent/file');
 467  
 468          $this->assertFalse($fs->is_file_readable_locally_by_storedfile($file));
 469      }
 470  
 471      /**
 472       * Test the stock implementation of is_file_readable_locally_by_storedfile with a valid file should pass fetch.
 473       *
 474       * @covers ::is_file_readable_locally_by_storedfile
 475       */
 476      public function test_is_file_readable_locally_by_storedfile_passes_fetch() {
 477          $fs = $this->get_testable_mock([
 478              'get_local_path_from_storedfile',
 479          ]);
 480          $file = $this->get_stored_file('example content');
 481  
 482          $fs->method('get_local_path_from_storedfile')
 483              ->with($this->equalTo($file), $this->equalTo(true))
 484              ->willReturn('/path/to/nonexistent/file');
 485  
 486          $this->assertFalse($fs->is_file_readable_locally_by_storedfile($file, true));
 487      }
 488  
 489      /**
 490       * Ensure that is_file_removable returns correctly for an empty file.
 491       *
 492       * @covers ::is_file_removable
 493       */
 494      public function test_is_file_removable_empty() {
 495          $filecontent = '';
 496          $contenthash = file_storage::hash_from_string($filecontent);
 497  
 498          $method = new ReflectionMethod(file_system::class, 'is_file_removable');
 499          $method->setAccessible(true);
 500          $result = $method->invokeArgs(null, [$contenthash]);
 501          $this->assertFalse($result);
 502      }
 503  
 504      /**
 505       * Ensure that is_file_removable returns false if the file is still in use.
 506       *
 507       * @covers ::is_file_removable
 508       */
 509      public function test_is_file_removable_in_use() {
 510          $this->resetAfterTest();
 511          global $DB;
 512  
 513          $filecontent = 'example content';
 514          $contenthash = file_storage::hash_from_string($filecontent);
 515  
 516          $DB = $this->getMockBuilder(\moodle_database::class)
 517              ->setMethods(['record_exists'])
 518              ->getMockForAbstractClass();
 519          $DB->method('record_exists')->willReturn(true);
 520  
 521          $method = new ReflectionMethod(file_system::class, 'is_file_removable');
 522          $method->setAccessible(true);
 523          $result = $method->invokeArgs(null, [$contenthash]);
 524  
 525          $this->assertFalse($result);
 526      }
 527  
 528      /**
 529       * Ensure that is_file_removable returns false if the file is not in use.
 530       *
 531       * @covers ::is_file_removable
 532       */
 533      public function test_is_file_removable_not_in_use() {
 534          $this->resetAfterTest();
 535          global $DB;
 536  
 537          $filecontent = 'example content';
 538          $contenthash = file_storage::hash_from_string($filecontent);
 539  
 540          $DB = $this->getMockBuilder(\moodle_database::class)
 541              ->setMethods(['record_exists'])
 542              ->getMockForAbstractClass();
 543          $DB->method('record_exists')->willReturn(false);
 544  
 545          $method = new ReflectionMethod(file_system::class, 'is_file_removable');
 546          $method->setAccessible(true);
 547          $result = $method->invokeArgs(null, [$contenthash]);
 548  
 549          $this->assertTrue($result);
 550      }
 551  
 552      /**
 553       * Test the stock implementation of get_content.
 554       *
 555       * @covers ::get_content
 556       */
 557      public function test_get_content() {
 558          global $CFG;
 559  
 560          // Mock the filesystem.
 561          $filecontent = 'example content';
 562          $vfileroot = $this->setup_vfile_root(['sourcefile' => $filecontent]);
 563          $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcefile');
 564  
 565          $file = $this->get_stored_file($filecontent);
 566  
 567          // Mock the file_system class.
 568          // We need to override the get_remote_path_from_storedfile function.
 569          $fs = $this->get_testable_mock(['get_remote_path_from_storedfile']);
 570          $fs->method('get_remote_path_from_storedfile')->willReturn($filepath);
 571  
 572          $result = $fs->get_content($file);
 573  
 574          $this->assertEquals($filecontent, $result);
 575      }
 576  
 577      /**
 578       * Test the stock implementation of get_content.
 579       *
 580       * @covers ::get_content
 581       */
 582      public function test_get_content_empty() {
 583          global $CFG;
 584  
 585          $filecontent = '';
 586          $file = $this->get_stored_file($filecontent);
 587  
 588          // Mock the file_system class.
 589          // We need to override the get_remote_path_from_storedfile function.
 590          $fs = $this->get_testable_mock(['get_remote_path_from_storedfile']);
 591          $fs->expects($this->never())
 592              ->method('get_remote_path_from_storedfile');
 593  
 594          $result = $fs->get_content($file);
 595  
 596          $this->assertEquals($filecontent, $result);
 597      }
 598  
 599      /**
 600       * Ensure that the list_files function requires a local copy of the
 601       * file, and passes the path to the packer.
 602       *
 603       * @covers ::list_files
 604       */
 605      public function test_list_files() {
 606          $filecontent = 'example content';
 607          $file = $this->get_stored_file($filecontent);
 608          $filepath = __FILE__;
 609          $expectedresult = (object) [];
 610  
 611          // Mock the file_system class.
 612          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
 613          $fs->method('get_local_path_from_storedfile')
 614              ->with($this->equalTo($file), $this->equalTo(true))
 615              ->willReturn(__FILE__);
 616  
 617          $packer = $this->getMockBuilder(file_packer::class)
 618              ->setMethods(['list_files'])
 619              ->getMockForAbstractClass();
 620  
 621          $packer->expects($this->once())
 622              ->method('list_files')
 623              ->with($this->equalTo($filepath))
 624              ->willReturn($expectedresult);
 625  
 626          $result = $fs->list_files($file, $packer);
 627  
 628          $this->assertEquals($expectedresult, $result);
 629      }
 630  
 631      /**
 632       * Ensure that the extract_to_pathname function requires a local copy of the
 633       * file, and passes the path to the packer.
 634       *
 635       * @covers ::extract_to_pathname
 636       */
 637      public function test_extract_to_pathname() {
 638          $filecontent = 'example content';
 639          $file = $this->get_stored_file($filecontent);
 640          $filepath = __FILE__;
 641          $expectedresult = (object) [];
 642          $outputpath = '/path/to/output';
 643  
 644          // Mock the file_system class.
 645          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
 646          $fs->method('get_local_path_from_storedfile')
 647              ->with($this->equalTo($file), $this->equalTo(true))
 648              ->willReturn(__FILE__);
 649  
 650          $packer = $this->getMockBuilder(file_packer::class)
 651              ->setMethods(['extract_to_pathname'])
 652              ->getMockForAbstractClass();
 653  
 654          $packer->expects($this->once())
 655              ->method('extract_to_pathname')
 656              ->with($this->equalTo($filepath), $this->equalTo($outputpath), $this->equalTo(null), $this->equalTo(null))
 657              ->willReturn($expectedresult);
 658  
 659          $result = $fs->extract_to_pathname($file, $packer, $outputpath);
 660  
 661          $this->assertEquals($expectedresult, $result);
 662      }
 663  
 664      /**
 665       * Ensure that the extract_to_storage function requires a local copy of the
 666       * file, and passes the path to the packer.
 667       *
 668       * @covers ::extract_to_storage
 669       */
 670      public function test_extract_to_storage() {
 671          $filecontent = 'example content';
 672          $file = $this->get_stored_file($filecontent);
 673          $filepath = __FILE__;
 674          $expectedresult = (object) [];
 675          $outputpath = '/path/to/output';
 676  
 677          // Mock the file_system class.
 678          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
 679          $fs->method('get_local_path_from_storedfile')
 680              ->with($this->equalTo($file), $this->equalTo(true))
 681              ->willReturn(__FILE__);
 682  
 683          $packer = $this->getMockBuilder(file_packer::class)
 684              ->setMethods(['extract_to_storage'])
 685              ->getMockForAbstractClass();
 686  
 687          $packer->expects($this->once())
 688              ->method('extract_to_storage')
 689              ->with(
 690                  $this->equalTo($filepath),
 691                  $this->equalTo(42),
 692                  $this->equalTo('component'),
 693                  $this->equalTo('filearea'),
 694                  $this->equalTo('itemid'),
 695                  $this->equalTo('pathbase'),
 696                  $this->equalTo('userid'),
 697                  $this->equalTo(null)
 698              )
 699              ->willReturn($expectedresult);
 700  
 701          $result = $fs->extract_to_storage($file, $packer, 42, 'component','filearea', 'itemid', 'pathbase', 'userid');
 702  
 703          $this->assertEquals($expectedresult, $result);
 704      }
 705  
 706      /**
 707       * Ensure that the add_storedfile_to_archive function requires a local copy of the
 708       * file, and passes the path to the archive.
 709       *
 710       */
 711      public function test_add_storedfile_to_archive_directory() {
 712          $file = $this->get_stored_file('', '.');
 713          $archivepath = 'example';
 714          $expectedresult = (object) [];
 715  
 716          // Mock the file_system class.
 717          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
 718          $fs->method('get_local_path_from_storedfile')
 719              ->with($this->equalTo($file), $this->equalTo(true))
 720              ->willReturn(__FILE__);
 721  
 722          $archive = $this->getMockBuilder(file_archive::class)
 723              ->setMethods([
 724                  'add_directory',
 725                  'add_file_from_pathname',
 726              ])
 727              ->getMockForAbstractClass();
 728  
 729          $archive->expects($this->once())
 730              ->method('add_directory')
 731              ->with($this->equalTo($archivepath))
 732              ->willReturn($expectedresult);
 733  
 734          $archive->expects($this->never())
 735              ->method('add_file_from_pathname');
 736  
 737          $result = $fs->add_storedfile_to_archive($file, $archive, $archivepath);
 738  
 739          $this->assertEquals($expectedresult, $result);
 740      }
 741  
 742      /**
 743       * Ensure that the add_storedfile_to_archive function requires a local copy of the
 744       * file, and passes the path to the archive.
 745       *
 746       */
 747      public function test_add_storedfile_to_archive_file() {
 748          $file = $this->get_stored_file('example content');
 749          $filepath = __LINE__;
 750          $archivepath = 'example';
 751          $expectedresult = (object) [];
 752  
 753          // Mock the file_system class.
 754          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
 755          $fs->method('get_local_path_from_storedfile')
 756              ->with($this->equalTo($file), $this->equalTo(true))
 757              ->willReturn($filepath);
 758  
 759          $archive = $this->getMockBuilder(file_archive::class)
 760              ->setMethods([
 761                  'add_directory',
 762                  'add_file_from_pathname',
 763              ])
 764              ->getMockForAbstractClass();
 765  
 766          $archive->expects($this->never())
 767              ->method('add_directory');
 768  
 769          $archive->expects($this->once())
 770              ->method('add_file_from_pathname')
 771              ->with(
 772                  $this->equalTo($archivepath),
 773                  $this->equalTo($filepath)
 774              )
 775              ->willReturn($expectedresult);
 776  
 777          $result = $fs->add_storedfile_to_archive($file, $archive, $archivepath);
 778  
 779          $this->assertEquals($expectedresult, $result);
 780      }
 781  
 782      /**
 783       * Ensure that the add_to_curl_request function requires a local copy of the
 784       * file, and passes the path to curl_file_create.
 785       *
 786       * @covers ::add_to_curl_request
 787       */
 788      public function test_add_to_curl_request() {
 789          $file = $this->get_stored_file('example content');
 790          $filepath = __FILE__;
 791          $archivepath = 'example';
 792          $key = 'myfile';
 793  
 794          // Mock the file_system class.
 795          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
 796          $fs->method('get_local_path_from_storedfile')
 797              ->with($this->equalTo($file), $this->equalTo(true))
 798              ->willReturn($filepath);
 799  
 800          $request = (object) ['_tmp_file_post_params' => []];
 801          $fs->add_to_curl_request($file, $request, $key);
 802          $this->assertArrayHasKey($key, $request->_tmp_file_post_params);
 803          $this->assertEquals($filepath, $request->_tmp_file_post_params[$key]->name);
 804      }
 805  
 806      /**
 807       * Ensure that test_get_imageinfo_not_image returns false if the file
 808       * passed was deemed to not be an image.
 809       *
 810       * @covers ::get_imageinfo
 811       */
 812      public function test_get_imageinfo_not_image() {
 813          $filecontent = 'example content';
 814          $file = $this->get_stored_file($filecontent);
 815  
 816          $fs = $this->get_testable_mock([
 817              'is_image_from_storedfile',
 818          ]);
 819  
 820          $fs->expects($this->once())
 821              ->method('is_image_from_storedfile')
 822              ->with($this->equalTo($file))
 823              ->willReturn(false);
 824  
 825          $this->assertFalse($fs->get_imageinfo($file));
 826      }
 827  
 828      /**
 829       * Ensure that test_get_imageinfo_not_image returns imageinfo.
 830       *
 831       * @covers ::get_imageinfo
 832       */
 833      public function test_get_imageinfo() {
 834          $filepath = '/path/to/file';
 835          $filecontent = 'example content';
 836          $expectedresult = (object) [];
 837          $file = $this->get_stored_file($filecontent);
 838  
 839          $fs = $this->get_testable_mock([
 840              'is_image_from_storedfile',
 841              'get_local_path_from_storedfile',
 842              'get_imageinfo_from_path',
 843          ]);
 844  
 845          $fs->expects($this->once())
 846              ->method('is_image_from_storedfile')
 847              ->with($this->equalTo($file))
 848              ->willReturn(true);
 849  
 850          $fs->expects($this->once())
 851              ->method('get_local_path_from_storedfile')
 852              ->with($this->equalTo($file), $this->equalTo(true))
 853              ->willReturn($filepath);
 854  
 855          $fs->expects($this->once())
 856              ->method('get_imageinfo_from_path')
 857              ->with($this->equalTo($filepath))
 858              ->willReturn($expectedresult);
 859  
 860          $this->assertEquals($expectedresult, $fs->get_imageinfo($file));
 861      }
 862  
 863      /**
 864       * Ensure that is_image_from_storedfile always returns false for an
 865       * empty file size.
 866       *
 867       * @covers ::is_image_from_storedfile
 868       */
 869      public function test_is_image_empty_filesize() {
 870          $filecontent = 'example content';
 871          $file = $this->get_stored_file($filecontent, null, ['get_filesize']);
 872  
 873          $file->expects($this->once())
 874              ->method('get_filesize')
 875              ->willReturn(0);
 876  
 877          $fs = $this->get_testable_mock();
 878          $this->assertFalse($fs->is_image_from_storedfile($file));
 879      }
 880  
 881      /**
 882       * Ensure that is_image_from_storedfile behaves correctly based on
 883       * mimetype.
 884       *
 885       * @dataProvider is_image_from_storedfile_provider
 886       * @param   string  $mimetype Mimetype to test
 887       * @param   bool    $isimage Whether this mimetype should be detected as an image
 888       * @covers ::is_image_from_storedfile
 889       */
 890      public function test_is_image_from_storedfile_mimetype($mimetype, $isimage) {
 891          $filecontent = 'example content';
 892          $file = $this->get_stored_file($filecontent, null, ['get_mimetype']);
 893  
 894          $file->expects($this->once())
 895              ->method('get_mimetype')
 896              ->willReturn($mimetype);
 897  
 898          $fs = $this->get_testable_mock();
 899          $this->assertEquals($isimage, $fs->is_image_from_storedfile($file));
 900      }
 901  
 902      /**
 903       * Test that get_imageinfo_from_path returns an appropriate response
 904       * for an image.
 905       *
 906       * @covers ::get_imageinfo_from_path
 907       */
 908      public function test_get_imageinfo_from_path() {
 909          $filepath = __DIR__ . "/fixtures/testimage.jpg";
 910  
 911          // Get the filesystem mock.
 912          $fs = $this->get_testable_mock();
 913  
 914          $method = new ReflectionMethod(file_system::class, 'get_imageinfo_from_path');
 915          $method->setAccessible(true);
 916          $result = $method->invokeArgs($fs, [$filepath]);
 917  
 918          $this->assertArrayHasKey('width', $result);
 919          $this->assertArrayHasKey('height', $result);
 920          $this->assertArrayHasKey('mimetype', $result);
 921          $this->assertEquals('image/jpeg', $result['mimetype']);
 922      }
 923  
 924      /**
 925       * Test that get_imageinfo_from_path returns an appropriate response
 926       * for a file which is not an image.
 927       *
 928       * @covers ::get_imageinfo_from_path
 929       */
 930      public function test_get_imageinfo_from_path_no_image() {
 931          $filepath = __FILE__;
 932  
 933          // Get the filesystem mock.
 934          $fs = $this->get_testable_mock();
 935  
 936          $method = new ReflectionMethod(file_system::class, 'get_imageinfo_from_path');
 937          $method->setAccessible(true);
 938          $result = $method->invokeArgs($fs, [$filepath]);
 939  
 940          $this->assertFalse($result);
 941      }
 942  
 943      /**
 944       * Test that get_imageinfo_from_path returns an appropriate response
 945       * for an svg image with viewbox attribute.
 946       */
 947      public function test_get_imageinfo_from_path_svg_viewbox() {
 948          $filepath = __DIR__ . '/fixtures/testimage_viewbox.svg';
 949  
 950          // Get the filesystem mock.
 951          $fs = $this->get_testable_mock();
 952  
 953          $method = new ReflectionMethod(file_system::class, 'get_imageinfo_from_path');
 954          $method->setAccessible(true);
 955          $result = $method->invokeArgs($fs, [$filepath]);
 956  
 957          $this->assertArrayHasKey('width', $result);
 958          $this->assertArrayHasKey('height', $result);
 959          $this->assertArrayHasKey('mimetype', $result);
 960          $this->assertEquals(100, $result['width']);
 961          $this->assertEquals(100, $result['height']);
 962          $this->assertStringContainsString('image/svg', $result['mimetype']);
 963      }
 964  
 965      /**
 966       * Test that get_imageinfo_from_path returns an appropriate response
 967       * for an svg image with width and height attributes.
 968       */
 969      public function test_get_imageinfo_from_path_svg_with_width_height() {
 970          $filepath = __DIR__ . '/fixtures/testimage_width_height.svg';
 971  
 972          // Get the filesystem mock.
 973          $fs = $this->get_testable_mock();
 974  
 975          $method = new ReflectionMethod(file_system::class, 'get_imageinfo_from_path');
 976          $method->setAccessible(true);
 977          $result = $method->invokeArgs($fs, [$filepath]);
 978  
 979          $this->assertArrayHasKey('width', $result);
 980          $this->assertArrayHasKey('height', $result);
 981          $this->assertArrayHasKey('mimetype', $result);
 982          $this->assertEquals(100, $result['width']);
 983          $this->assertEquals(100, $result['height']);
 984          $this->assertStringContainsString('image/svg', $result['mimetype']);
 985      }
 986  
 987      /**
 988       * Test that get_imageinfo_from_path returns an appropriate response
 989       * for an svg image without attributes.
 990       */
 991      public function test_get_imageinfo_from_path_svg_without_attribute() {
 992          $filepath = __DIR__ . '/fixtures/testimage.svg';
 993  
 994          // Get the filesystem mock.
 995          $fs = $this->get_testable_mock();
 996  
 997          $method = new ReflectionMethod(file_system::class, 'get_imageinfo_from_path');
 998          $method->setAccessible(true);
 999          $result = $method->invokeArgs($fs, [$filepath]);
1000  
1001          $this->assertArrayHasKey('width', $result);
1002          $this->assertArrayHasKey('height', $result);
1003          $this->assertArrayHasKey('mimetype', $result);
1004          $this->assertEquals(800, $result['width']);
1005          $this->assertEquals(600, $result['height']);
1006          $this->assertStringContainsString('image/svg', $result['mimetype']);
1007      }
1008  
1009      /**
1010       * Test that get_imageinfo_from_path returns an appropriate response
1011       * for a file which is not an correct svg.
1012       */
1013      public function test_get_imageinfo_from_path_svg_invalid() {
1014          $filepath = __DIR__ . '/fixtures/testimage_error.svg';
1015  
1016          // Get the filesystem mock.
1017          $fs = $this->get_testable_mock();
1018  
1019          $method = new ReflectionMethod(file_system::class, 'get_imageinfo_from_path');
1020          $method->setAccessible(true);
1021          $result = $method->invokeArgs($fs, [$filepath]);
1022  
1023          $this->assertFalse($result);
1024      }
1025  
1026      /**
1027       * Ensure that get_content_file_handle returns a valid file handle.
1028       *
1029       * @covers ::get_content_file_handle
1030       */
1031      public function test_get_content_file_handle_default() {
1032          $filecontent = 'example content';
1033          $file = $this->get_stored_file($filecontent);
1034  
1035          $fs = $this->get_testable_mock(['get_remote_path_from_storedfile']);
1036          $fs->method('get_remote_path_from_storedfile')
1037              ->willReturn(__FILE__);
1038  
1039          // Note: We are unable to determine the mode in which the $fh was opened.
1040          $fh = $fs->get_content_file_handle($file);
1041          $this->assertTrue(is_resource($fh));
1042          $this->assertEquals('stream', get_resource_type($fh));
1043          fclose($fh);
1044      }
1045  
1046      /**
1047       * Ensure that get_content_file_handle returns a valid file handle for a gz file.
1048       *
1049       * @covers ::get_content_file_handle
1050       */
1051      public function test_get_content_file_handle_gz() {
1052          $filecontent = 'example content';
1053          $file = $this->get_stored_file($filecontent);
1054  
1055          $fs = $this->get_testable_mock(['get_local_path_from_storedfile']);
1056          $fs->method('get_local_path_from_storedfile')
1057              ->willReturn(__DIR__ . "/fixtures/test.tgz");
1058  
1059          // Note: We are unable to determine the mode in which the $fh was opened.
1060          $fh = $fs->get_content_file_handle($file, stored_file::FILE_HANDLE_GZOPEN);
1061          $this->assertTrue(is_resource($fh));
1062          gzclose($fh);
1063      }
1064  
1065      /**
1066       * Ensure that get_content_file_handle returns an exception when calling for a invalid file handle type.
1067       *
1068       * @covers ::get_content_file_handle
1069       */
1070      public function test_get_content_file_handle_invalid() {
1071          $filecontent = 'example content';
1072          $file = $this->get_stored_file($filecontent);
1073  
1074          $fs = $this->get_testable_mock(['get_remote_path_from_storedfile']);
1075          $fs->method('get_remote_path_from_storedfile')
1076              ->willReturn(__FILE__);
1077  
1078          $this->expectException('coding_exception', 'Unexpected file handle type');
1079          $fs->get_content_file_handle($file, -1);
1080      }
1081  
1082      /**
1083       * Test that mimetype_from_hash returns the correct mimetype with
1084       * a file whose filename suggests mimetype.
1085       *
1086       * @covers ::mimetype_from_hash
1087       */
1088      public function test_mimetype_from_hash_using_filename() {
1089          $filepath = '/path/to/file/not/currently/on/disk';
1090          $filecontent = 'example content';
1091          $filename = 'test.jpg';
1092          $contenthash = file_storage::hash_from_string($filecontent);
1093  
1094          $fs = $this->get_testable_mock(['get_remote_path_from_hash']);
1095          $fs->method('get_remote_path_from_hash')->willReturn($filepath);
1096  
1097          $result = $fs->mimetype_from_hash($contenthash, $filename);
1098          $this->assertEquals('image/jpeg', $result);
1099      }
1100  
1101      /**
1102       * Test that mimetype_from_hash returns the correct mimetype with
1103       * a locally available file whose filename does not suggest mimetype.
1104       *
1105       * @covers ::mimetype_from_hash
1106       */
1107      public function test_mimetype_from_hash_using_file_content() {
1108          $filecontent = 'example content';
1109          $contenthash = file_storage::hash_from_string($filecontent);
1110          $filename = 'example';
1111  
1112          $filepath = __DIR__ . "/fixtures/testimage.jpg";
1113          $fs = $this->get_testable_mock(['get_local_path_from_hash']);
1114          $fs->method('get_local_path_from_hash')->willReturn($filepath);
1115  
1116          $result = $fs->mimetype_from_hash($contenthash, $filename);
1117          $this->assertEquals('image/jpeg', $result);
1118      }
1119  
1120      /**
1121       * Test that mimetype_from_hash returns the correct mimetype with
1122       * a remotely available file whose filename does not suggest mimetype.
1123       *
1124       * @covers ::mimetype_from_hash
1125       */
1126      public function test_mimetype_from_hash_using_file_content_remote() {
1127          $filepath = '/path/to/file/not/currently/on/disk';
1128          $filecontent = 'example content';
1129          $contenthash = file_storage::hash_from_string($filecontent);
1130          $filename = 'example';
1131  
1132          $filepath = __DIR__ . "/fixtures/testimage.jpg";
1133  
1134          $fs = $this->get_testable_mock([
1135              'get_remote_path_from_hash',
1136              'is_file_readable_locally_by_hash',
1137              'get_local_path_from_hash',
1138          ]);
1139  
1140          $fs->method('get_remote_path_from_hash')->willReturn('/path/to/remote/file');
1141          $fs->method('is_file_readable_locally_by_hash')->willReturn(false);
1142          $fs->method('get_local_path_from_hash')->willReturn($filepath);
1143  
1144          $result = $fs->mimetype_from_hash($contenthash, $filename);
1145          $this->assertEquals('image/jpeg', $result);
1146      }
1147  
1148      /**
1149       * Test that mimetype_from_storedfile returns the correct mimetype with
1150       * a file whose filename suggests mimetype.
1151       *
1152       * @covers ::mimetype_from_storedfile
1153       */
1154      public function test_mimetype_from_storedfile_empty() {
1155          $file = $this->get_stored_file('');
1156  
1157          $fs = $this->get_testable_mock();
1158          $result = $fs->mimetype_from_storedfile($file);
1159          $this->assertNull($result);
1160      }
1161  
1162      /**
1163       * Test that mimetype_from_storedfile returns the correct mimetype with
1164       * a file whose filename suggests mimetype.
1165       *
1166       * @covers ::mimetype_from_storedfile
1167       */
1168      public function test_mimetype_from_storedfile_using_filename() {
1169          $filepath = '/path/to/file/not/currently/on/disk';
1170          $fs = $this->get_testable_mock(['get_remote_path_from_storedfile']);
1171          $fs->method('get_remote_path_from_storedfile')->willReturn($filepath);
1172  
1173          $file = $this->get_stored_file('example content', 'test.jpg');
1174  
1175          $result = $fs->mimetype_from_storedfile($file);
1176          $this->assertEquals('image/jpeg', $result);
1177      }
1178  
1179      /**
1180       * Test that mimetype_from_storedfile returns the correct mimetype with
1181       * a locally available file whose filename does not suggest mimetype.
1182       *
1183       * @covers ::mimetype_from_storedfile
1184       */
1185      public function test_mimetype_from_storedfile_using_file_content() {
1186          $filepath = __DIR__ . "/fixtures/testimage.jpg";
1187          $fs = $this->get_testable_mock(['get_local_path_from_hash']);
1188          $fs->method('get_local_path_from_hash')->willReturn($filepath);
1189  
1190          $file = $this->get_stored_file('example content');
1191  
1192          $result = $fs->mimetype_from_storedfile($file);
1193          $this->assertEquals('image/jpeg', $result);
1194      }
1195  
1196      /**
1197       * Test that mimetype_from_storedfile returns the correct mimetype with
1198       * a remotely available file whose filename does not suggest mimetype.
1199       *
1200       * @covers ::mimetype_from_storedfile
1201       */
1202      public function test_mimetype_from_storedfile_using_file_content_remote() {
1203          $filepath = __DIR__ . "/fixtures/testimage.jpg";
1204  
1205          $fs = $this->get_testable_mock([
1206              'is_file_readable_locally_by_hash',
1207              'get_local_path_from_hash',
1208          ]);
1209  
1210          $fs->method('is_file_readable_locally_by_hash')->willReturn(false);
1211          $fs->method('get_local_path_from_hash')->will($this->onConsecutiveCalls('/path/to/remote/file', $filepath));
1212  
1213          $file = $this->get_stored_file('example content');
1214  
1215          $result = $fs->mimetype_from_storedfile($file);
1216          $this->assertEquals('image/jpeg', $result);
1217      }
1218  
1219      /**
1220       * Data Provider for is_image_from_storedfile tests.
1221       *
1222       * @return array
1223       */
1224      public function is_image_from_storedfile_provider() {
1225          return array(
1226              'Standard image'            => array('image/png', true),
1227              'Made up document/image'    => array('document/image', false),
1228          );
1229      }
1230  
1231      /**
1232       * Data provider for get_local_path_from_storedfile tests.
1233       *
1234       * @return array
1235       */
1236      public function get_local_path_from_storedfile_provider() {
1237          return [
1238              'default args (nofetch)' => [
1239                  'args' => [],
1240                  'fetch' => 0,
1241              ],
1242              'explicit: nofetch' => [
1243                  'args' => [false],
1244                  'fetch' => 0,
1245              ],
1246              'explicit: fetch' => [
1247                  'args' => [true],
1248                  'fetch' => 1,
1249              ],
1250          ];
1251      }
1252  }
1253