Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

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