Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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  /**
  18   * Unit tests for file_system_filedir.
  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  require_once($CFG->libdir . '/filestorage/file_system_filedir.php');
  31  
  32  /**
  33   * Unit tests for file_system_filedir.
  34   *
  35   * @package   core_files
  36   * @category  files
  37   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   * @coversDefaultClass \file_system_filedir
  40   */
  41  class core_files_file_system_filedir_testcase extends advanced_testcase {
  42  
  43      /**
  44       * Shared test setUp.
  45       */
  46      public function setUp() {
  47          // Reset the file storage so that subsequent fetches to get_file_storage are called after
  48          // configuration is prepared.
  49          get_file_storage(true);
  50      }
  51  
  52      /**
  53       * Shared teset tearDown.
  54       */
  55      public function tearDown() {
  56          // Reset the file storage so that subsequent tests will use the standard file storage.
  57          get_file_storage(true);
  58      }
  59  
  60      /**
  61       * Helper function to help setup and configure the virtual file system stream.
  62       *
  63       * @param   array $filedir Directory structure and content of the filedir
  64       * @param   array $trashdir Directory structure and content of the sourcedir
  65       * @param   array $sourcedir Directory structure and content of a directory used for source files for tests
  66       * @return  \org\bovigo\vfs\vfsStream
  67       */
  68      protected function setup_vfile_root($filedir = [], $trashdir = [], $sourcedir = null) {
  69          global $CFG;
  70          $this->resetAfterTest();
  71  
  72          $content = [];
  73          if ($filedir !== null) {
  74              $content['filedir'] = $filedir;
  75          }
  76  
  77          if ($trashdir !== null) {
  78              $content['trashdir'] = $trashdir;
  79          }
  80  
  81          if ($sourcedir !== null) {
  82              $content['sourcedir'] = $sourcedir;
  83          }
  84  
  85          $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
  86  
  87          $CFG->filedir = \org\bovigo\vfs\vfsStream::url('root/filedir');
  88          $CFG->trashdir = \org\bovigo\vfs\vfsStream::url('root/trashdir');
  89  
  90          return $vfileroot;
  91      }
  92  
  93      /**
  94       * Helper to create a stored file objectw with the given supplied content.
  95       *
  96       * @param   string  $filecontent The content of the mocked file
  97       * @param   string  $filename The file name to use in the stored_file
  98       * @param   array   $mockedmethods A list of methods you intend to override
  99       *                  If no methods are specified, only abstract functions are mocked.
 100       * @return stored_file
 101       */
 102      protected function get_stored_file($filecontent, $filename = null, $mockedmethods = null) {
 103          $contenthash = file_storage::hash_from_string($filecontent);
 104          if (empty($filename)) {
 105              $filename = $contenthash;
 106          }
 107  
 108          $file = $this->getMockBuilder(stored_file::class)
 109              ->setMethods($mockedmethods)
 110              ->setConstructorArgs([
 111                  get_file_storage(),
 112                  (object) [
 113                      'contenthash' => $contenthash,
 114                      'filesize' => strlen($filecontent),
 115                      'filename' => $filename,
 116                  ]
 117              ])
 118              ->getMock();
 119  
 120          return $file;
 121      }
 122  
 123      /**
 124       * Get a testable mock of the file_system_filedir class.
 125       *
 126       * @param   array   $mockedmethods A list of methods you intend to override
 127       *                  If no methods are specified, only abstract functions are mocked.
 128       * @return file_system
 129       */
 130      protected function get_testable_mock($mockedmethods = []) {
 131          $fs = $this->getMockBuilder(file_system_filedir::class)
 132              ->setMethods($mockedmethods)
 133              ->getMock();
 134  
 135          return $fs;
 136      }
 137  
 138      /**
 139       * Ensure that an appropriate error is shown when the filedir directory
 140       * is not writable.
 141       *
 142       * @covers ::__construct
 143       */
 144      public function test_readonly_filesystem_filedir() {
 145          $this->resetAfterTest();
 146  
 147          // Setup the filedir but remove permissions.
 148          $vfileroot = $this->setup_vfile_root(null);
 149  
 150          // Make the target path readonly.
 151          $vfileroot->chmod(0444)
 152              ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 153  
 154          // This should generate an exception.
 155          $this->expectException('file_exception');
 156          $this->expectExceptionMessageRegExp(
 157              '/Cannot create local file pool directories. Please verify permissions in dataroot./');
 158  
 159          new file_system_filedir();
 160      }
 161  
 162      /**
 163       * Ensure that an appropriate error is shown when the trash directory
 164       * is not writable.
 165       *
 166       * @covers ::__construct
 167       */
 168      public function test_readonly_filesystem_trashdir() {
 169          $this->resetAfterTest();
 170  
 171          // Setup the trashdir but remove permissions.
 172          $vfileroot = $this->setup_vfile_root([], null);
 173  
 174          // Make the target path readonly.
 175          $vfileroot->chmod(0444)
 176              ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 177  
 178          // This should generate an exception.
 179          $this->expectException('file_exception');
 180          $this->expectExceptionMessageRegExp(
 181              '/Cannot create local file pool directories. Please verify permissions in dataroot./');
 182  
 183          new file_system_filedir();
 184      }
 185  
 186      /**
 187       * Test that the standard Moodle warning message is put into the filedir.
 188       *
 189       * @covers ::__construct
 190       */
 191      public function test_warnings_put_in_place() {
 192          $this->resetAfterTest();
 193  
 194          $vfileroot = $this->setup_vfile_root(null);
 195  
 196          new file_system_filedir();
 197          $this->assertTrue($vfileroot->hasChild('filedir/warning.txt'));
 198          $this->assertEquals(
 199              'This directory contains the content of uploaded files and is controlled by Moodle code. ' .
 200                  'Do not manually move, change or rename any of the files and subdirectories here.',
 201              $vfileroot->getChild('filedir/warning.txt')->getContent()
 202          );
 203      }
 204  
 205      /**
 206       * Ensure that the default implementation of get_remote_path_from_hash
 207       * simply calls get_local_path_from_hash.
 208       *
 209       * @covers ::get_remote_path_from_hash
 210       */
 211      public function test_get_remote_path_from_hash() {
 212          $filecontent = 'example content';
 213          $contenthash = file_storage::hash_from_string($filecontent);
 214          $expectedresult = (object) [];
 215  
 216          $fs = $this->get_testable_mock([
 217              'get_local_path_from_hash',
 218          ]);
 219  
 220          $fs->expects($this->once())
 221              ->method('get_local_path_from_hash')
 222              ->with($this->equalTo($contenthash), $this->equalTo(false))
 223              ->willReturn($expectedresult);
 224  
 225          $method = new ReflectionMethod(file_system_filedir::class, 'get_remote_path_from_hash');
 226          $method->setAccessible(true);
 227          $result = $method->invokeArgs($fs, [$contenthash]);
 228  
 229          $this->assertEquals($expectedresult, $result);
 230      }
 231  
 232      /**
 233       * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
 234       * a failed recovery.
 235       *
 236       * @covers ::get_local_path_from_storedfile
 237       */
 238      public function test_get_local_path_from_storedfile_with_recovery() {
 239          $filecontent = 'example content';
 240          $file = $this->get_stored_file($filecontent);
 241          $fs = $this->get_testable_mock([
 242              'get_local_path_from_hash',
 243              'recover_file',
 244          ]);
 245          $filepath = '/path/to/nonexistent/file';
 246  
 247          $fs->method('get_local_path_from_hash')
 248              ->willReturn($filepath);
 249  
 250          $fs->expects($this->once())
 251              ->method('recover_file')
 252              ->with($this->equalTo($file));
 253  
 254          $file = $this->get_stored_file('example content');
 255          $result = $fs->get_local_path_from_storedfile($file, true);
 256  
 257          $this->assertEquals($filepath, $result);
 258      }
 259  
 260      /**
 261       * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
 262       * a failed recovery.
 263       *
 264       * @covers ::get_local_path_from_storedfile
 265       */
 266      public function test_get_local_path_from_storedfile_without_recovery() {
 267          $filecontent = 'example content';
 268          $file = $this->get_stored_file($filecontent);
 269          $fs = $this->get_testable_mock([
 270              'get_local_path_from_hash',
 271              'recover_file',
 272          ]);
 273          $filepath = '/path/to/nonexistent/file';
 274  
 275          $fs->method('get_local_path_from_hash')
 276              ->willReturn($filepath);
 277  
 278          $fs->expects($this->never())
 279              ->method('recover_file');
 280  
 281          $file = $this->get_stored_file('example content');
 282          $result = $fs->get_local_path_from_storedfile($file, false);
 283  
 284          $this->assertEquals($filepath, $result);
 285      }
 286  
 287      /**
 288       * Test that the correct path is generated for the supplied content
 289       * hashes.
 290       *
 291       * @dataProvider contenthash_dataprovider
 292       * @param   string  $hash contenthash to test
 293       * @param   string  $hashdir Expected format of content directory
 294       *
 295       * @covers ::get_fulldir_from_hash
 296       */
 297      public function test_get_fulldir_from_hash($hash, $hashdir) {
 298          global $CFG;
 299  
 300          $fs = new file_system_filedir();
 301          $method = new ReflectionMethod(file_system_filedir::class, 'get_fulldir_from_hash');
 302          $method->setAccessible(true);
 303          $result = $method->invokeArgs($fs, array($hash));
 304  
 305          $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
 306          $this->assertEquals($expectedpath, $result);
 307      }
 308  
 309      /**
 310       * Test that the correct path is generated for the supplied content
 311       * hashes when used with a stored_file.
 312       *
 313       * @dataProvider contenthash_dataprovider
 314       * @param   string  $hash contenthash to test
 315       * @param   string  $hashdir Expected format of content directory
 316       *
 317       * @covers ::get_fulldir_from_storedfile
 318       */
 319      public function test_get_fulldir_from_storedfile($hash, $hashdir) {
 320          global $CFG;
 321  
 322          $file = $this->getMockBuilder('stored_file')
 323              ->disableOriginalConstructor()
 324              ->setMethods([
 325                  'sync_external_file',
 326                  'get_contenthash',
 327              ])
 328              ->getMock();
 329  
 330          $file->method('get_contenthash')->willReturn($hash);
 331  
 332          $fs = new file_system_filedir();
 333          $method = new ReflectionMethod('file_system_filedir', 'get_fulldir_from_storedfile');
 334          $method->setAccessible(true);
 335          $result = $method->invokeArgs($fs, array($file));
 336  
 337          $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
 338          $this->assertEquals($expectedpath, $result);
 339      }
 340  
 341      /**
 342       * Test that the correct content directory is generated for the supplied
 343       * content hashes.
 344       *
 345       * @dataProvider contenthash_dataprovider
 346       * @param   string  $hash contenthash to test
 347       * @param   string  $hashdir Expected format of content directory
 348       *
 349       * @covers ::get_contentdir_from_hash
 350       */
 351      public function test_get_contentdir_from_hash($hash, $hashdir) {
 352          $method = new ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash');
 353          $method->setAccessible(true);
 354  
 355          $fs = new file_system_filedir();
 356          $result = $method->invokeArgs($fs, array($hash));
 357  
 358          $this->assertEquals($hashdir, $result);
 359      }
 360  
 361      /**
 362       * Test that the correct content path is generated for the supplied
 363       * content hashes.
 364       *
 365       * @dataProvider contenthash_dataprovider
 366       * @param   string  $hash contenthash to test
 367       * @param   string  $hashdir Expected format of content directory
 368       *
 369       * @covers ::get_contentpath_from_hash
 370       */
 371      public function test_get_contentpath_from_hash($hash, $hashdir) {
 372          $method = new ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash');
 373          $method->setAccessible(true);
 374  
 375          $fs = new file_system_filedir();
 376          $result = $method->invokeArgs($fs, array($hash));
 377  
 378          $expectedpath = sprintf('%s/%s', $hashdir, $hash);
 379          $this->assertEquals($expectedpath, $result);
 380      }
 381  
 382      /**
 383       * Test that the correct trash path is generated for the supplied
 384       * content hashes.
 385       *
 386       * @dataProvider contenthash_dataprovider
 387       * @param   string  $hash contenthash to test
 388       * @param   string  $hashdir Expected format of content directory
 389       *
 390       * @covers ::get_trash_fullpath_from_hash
 391       */
 392      public function test_get_trash_fullpath_from_hash($hash, $hashdir) {
 393          global $CFG;
 394  
 395          $fs = new file_system_filedir();
 396          $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fullpath_from_hash');
 397          $method->setAccessible(true);
 398          $result = $method->invokeArgs($fs, array($hash));
 399  
 400          $expectedpath = sprintf('%s/trashdir/%s/%s', $CFG->dataroot, $hashdir, $hash);
 401          $this->assertEquals($expectedpath, $result);
 402      }
 403  
 404      /**
 405       * Test that the correct trash directory is generated for the supplied
 406       * content hashes.
 407       *
 408       * @dataProvider contenthash_dataprovider
 409       * @param   string  $hash contenthash to test
 410       * @param   string  $hashdir Expected format of content directory
 411       *
 412       * @covers ::get_trash_fulldir_from_hash
 413       */
 414      public function test_get_trash_fulldir_from_hash($hash, $hashdir) {
 415          global $CFG;
 416  
 417          $fs = new file_system_filedir();
 418          $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fulldir_from_hash');
 419          $method->setAccessible(true);
 420          $result = $method->invokeArgs($fs, array($hash));
 421  
 422          $expectedpath = sprintf('%s/trashdir/%s', $CFG->dataroot, $hashdir);
 423          $this->assertEquals($expectedpath, $result);
 424      }
 425  
 426      /**
 427       * Ensure that copying a file to a target from a stored_file works as anticipated.
 428       *
 429       * @covers ::copy_content_from_storedfile
 430       */
 431      public function test_copy_content_from_storedfile() {
 432          $this->resetAfterTest();
 433          global $CFG;
 434  
 435          $filecontent = 'example content';
 436          $contenthash = file_storage::hash_from_string($filecontent);
 437          $filedircontent = [
 438              $contenthash => $filecontent,
 439          ];
 440          $vfileroot = $this->setup_vfile_root($filedircontent, [], []);
 441  
 442          $fs = $this->getMockBuilder(file_system_filedir::class)
 443              ->disableOriginalConstructor()
 444              ->setMethods([
 445                  'get_local_path_from_storedfile',
 446              ])
 447              ->getMock();
 448  
 449          $file = $this->getMockBuilder(stored_file::class)
 450              ->disableOriginalConstructor()
 451              ->getMock();
 452  
 453          $sourcefile = \org\bovigo\vfs\vfsStream::url('root/filedir/' . $contenthash);
 454          $fs->method('get_local_path_from_storedfile')->willReturn($sourcefile);
 455  
 456          $targetfile = \org\bovigo\vfs\vfsStream::url('root/targetfile');
 457          $CFG->preventfilelocking = true;
 458          $result = $fs->copy_content_from_storedfile($file, $targetfile);
 459  
 460          $this->assertTrue($result);
 461          $this->assertEquals($filecontent, $vfileroot->getChild('targetfile')->getContent());
 462      }
 463  
 464      /**
 465       * Ensure that content recovery works.
 466       *
 467       * @covers ::recover_file
 468       */
 469      public function test_recover_file() {
 470          $this->resetAfterTest();
 471  
 472          // Setup the filedir.
 473          // This contains a virtual file which has a cache mismatch.
 474          $filecontent = 'example content';
 475          $contenthash = file_storage::hash_from_string($filecontent);
 476  
 477          $trashdircontent = [
 478              '0f' => [
 479                  'f3' => [
 480                      $contenthash => $filecontent,
 481                  ],
 482              ],
 483          ];
 484  
 485          $vfileroot = $this->setup_vfile_root([], $trashdircontent);
 486  
 487          $file = new stored_file(get_file_storage(), (object) [
 488              'contenthash' => $contenthash,
 489              'filesize' => strlen($filecontent),
 490          ]);
 491  
 492          $fs = new file_system_filedir();
 493          $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
 494          $method->setAccessible(true);
 495          $result = $method->invokeArgs($fs, array($file));
 496  
 497          // Test the output.
 498          $this->assertTrue($result);
 499  
 500          $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
 501  
 502      }
 503  
 504      /**
 505       * Ensure that content recovery works.
 506       *
 507       * @covers ::recover_file
 508       */
 509      public function test_recover_file_already_present() {
 510          $this->resetAfterTest();
 511  
 512          // Setup the filedir.
 513          // This contains a virtual file which has a cache mismatch.
 514          $filecontent = 'example content';
 515          $contenthash = file_storage::hash_from_string($filecontent);
 516  
 517          $filedircontent = $trashdircontent = [
 518              '0f' => [
 519                  'f3' => [
 520                      $contenthash => $filecontent,
 521                  ],
 522              ],
 523          ];
 524  
 525          $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
 526  
 527          $file = new stored_file(get_file_storage(), (object) [
 528              'contenthash' => $contenthash,
 529              'filesize' => strlen($filecontent),
 530          ]);
 531  
 532          $fs = new file_system_filedir();
 533          $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
 534          $method->setAccessible(true);
 535          $result = $method->invokeArgs($fs, array($file));
 536  
 537          // Test the output.
 538          $this->assertTrue($result);
 539  
 540          $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
 541      }
 542  
 543      /**
 544       * Ensure that content recovery works.
 545       *
 546       * @covers ::recover_file
 547       */
 548      public function test_recover_file_size_mismatch() {
 549          $this->resetAfterTest();
 550  
 551          // Setup the filedir.
 552          // This contains a virtual file which has a cache mismatch.
 553          $filecontent = 'example content';
 554          $contenthash = file_storage::hash_from_string($filecontent);
 555  
 556          $trashdircontent = [
 557              '0f' => [
 558                  'f3' => [
 559                      $contenthash => $filecontent,
 560                  ],
 561              ],
 562          ];
 563          $vfileroot = $this->setup_vfile_root([], $trashdircontent);
 564  
 565          $file = new stored_file(get_file_storage(), (object) [
 566              'contenthash' => $contenthash,
 567              'filesize' => strlen($filecontent) + 1,
 568          ]);
 569  
 570          $fs = new file_system_filedir();
 571          $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
 572          $method->setAccessible(true);
 573          $result = $method->invokeArgs($fs, array($file));
 574  
 575          // Test the output.
 576          $this->assertFalse($result);
 577          $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
 578      }
 579  
 580      /**
 581       * Ensure that content recovery works.
 582       *
 583       * @covers ::recover_file
 584       */
 585      public function test_recover_file_has_mismatch() {
 586          $this->resetAfterTest();
 587  
 588          // Setup the filedir.
 589          // This contains a virtual file which has a cache mismatch.
 590          $filecontent = 'example content';
 591          $contenthash = file_storage::hash_from_string($filecontent);
 592  
 593          $trashdircontent = [
 594              '0f' => [
 595                  'f3' => [
 596                      $contenthash => $filecontent,
 597                  ],
 598              ],
 599          ];
 600          $vfileroot = $this->setup_vfile_root([], $trashdircontent);
 601  
 602          $file = new stored_file(get_file_storage(), (object) [
 603              'contenthash' => $contenthash . " different",
 604              'filesize' => strlen($filecontent),
 605          ]);
 606  
 607          $fs = new file_system_filedir();
 608          $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
 609          $method->setAccessible(true);
 610          $result = $method->invokeArgs($fs, array($file));
 611  
 612          // Test the output.
 613          $this->assertFalse($result);
 614          $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
 615      }
 616  
 617      /**
 618       * Ensure that content recovery works when the content file is in the
 619       * alt trash directory.
 620       *
 621       * @covers ::recover_file
 622       */
 623      public function test_recover_file_alttrash() {
 624          $this->resetAfterTest();
 625  
 626          // Setup the filedir.
 627          // This contains a virtual file which has a cache mismatch.
 628          $filecontent = 'example content';
 629          $contenthash = file_storage::hash_from_string($filecontent);
 630  
 631          $trashdircontent = [
 632              $contenthash => $filecontent,
 633          ];
 634          $vfileroot = $this->setup_vfile_root([], $trashdircontent);
 635  
 636          $file = new stored_file(get_file_storage(), (object) [
 637              'contenthash' => $contenthash,
 638              'filesize' => strlen($filecontent),
 639          ]);
 640  
 641          $fs = new file_system_filedir();
 642          $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
 643          $method->setAccessible(true);
 644          $result = $method->invokeArgs($fs, array($file));
 645  
 646          // Test the output.
 647          $this->assertTrue($result);
 648  
 649          $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
 650      }
 651  
 652      /**
 653       * Test that an appropriate error message is generated when adding a
 654       * file to the pool when the pool directory structure is not writable.
 655       *
 656       * @covers ::recover_file
 657       */
 658      public function test_recover_file_contentdir_readonly() {
 659          $this->resetAfterTest();
 660  
 661          $filecontent = 'example content';
 662          $contenthash = file_storage::hash_from_string($filecontent);
 663          $filedircontent = [
 664              '0f' => [],
 665          ];
 666          $trashdircontent = [
 667              $contenthash => $filecontent,
 668          ];
 669          $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
 670  
 671          // Make the target path readonly.
 672          $vfileroot->getChild('filedir/0f')
 673              ->chmod(0444)
 674              ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 675  
 676          $file = new stored_file(get_file_storage(), (object) [
 677              'contenthash' => $contenthash,
 678              'filesize' => strlen($filecontent),
 679          ]);
 680  
 681          $fs = new file_system_filedir();
 682          $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
 683          $method->setAccessible(true);
 684          $result = $method->invokeArgs($fs, array($file));
 685  
 686          // Test the output.
 687          $this->assertFalse($result);
 688      }
 689  
 690      /**
 691       * Test adding a file to the pool.
 692       *
 693       * @covers ::add_file_from_path
 694       */
 695      public function test_add_file_from_path() {
 696          $this->resetAfterTest();
 697          global $CFG;
 698  
 699          // Setup the filedir.
 700          // This contains a virtual file which has a cache mismatch.
 701          $filecontent = 'example content';
 702          $contenthash = file_storage::hash_from_string($filecontent);
 703          $sourcedircontent = [
 704              'file' => $filecontent,
 705          ];
 706  
 707          $vfileroot = $this->setup_vfile_root([], [], $sourcedircontent);
 708  
 709          // Note, the vfs file system does not support locks - prevent file locking here.
 710          $CFG->preventfilelocking = true;
 711  
 712          // Attempt to add the file to the file pool.
 713          $fs = new file_system_filedir();
 714          $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
 715          $result = $fs->add_file_from_path($sourcefile);
 716  
 717          // Test the output.
 718          $this->assertEquals($contenthash, $result[0]);
 719          $this->assertEquals(core_text::strlen($filecontent), $result[1]);
 720          $this->assertTrue($result[2]);
 721  
 722          $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
 723      }
 724  
 725      /**
 726       * Test that an appropriate error message is generated when adding an
 727       * unavailable file to the pool is attempted.
 728       *
 729       * @covers ::add_file_from_path
 730       */
 731      public function test_add_file_from_path_file_unavailable() {
 732          $this->resetAfterTest();
 733  
 734          // Setup the filedir.
 735          $vfileroot = $this->setup_vfile_root();
 736  
 737          $this->expectException('file_exception');
 738          $this->expectExceptionMessageRegExp(
 739              '/Cannot read file\. Either the file does not exist or there is a permission problem\./');
 740  
 741          $fs = new file_system_filedir();
 742          $fs->add_file_from_path(\org\bovigo\vfs\vfsStream::url('filedir/file'));
 743      }
 744  
 745      /**
 746       * Test that an appropriate error message is generated when specifying
 747       * the wrong contenthash when adding a file to the pool.
 748       *
 749       * @covers ::add_file_from_path
 750       */
 751      public function test_add_file_from_path_mismatched_hash() {
 752          $this->resetAfterTest();
 753  
 754          $filecontent = 'example content';
 755          $contenthash = file_storage::hash_from_string($filecontent);
 756          $sourcedir = [
 757              'file' => $filecontent,
 758          ];
 759          $vfileroot = $this->setup_vfile_root([], [], $sourcedir);
 760  
 761          $fs = new file_system_filedir();
 762          $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
 763          $fs->add_file_from_path($filepath, 'eee4943847a35a4b6942c6f96daafde06bcfdfab');
 764          $this->assertDebuggingCalled("Invalid contenthash submitted for file $filepath");
 765      }
 766  
 767      /**
 768       * Test that an appropriate error message is generated when an existing
 769       * file in the pool has the wrong contenthash
 770       *
 771       * @covers ::add_file_from_path
 772       */
 773      public function test_add_file_from_path_existing_content_invalid() {
 774          $this->resetAfterTest();
 775  
 776          $filecontent = 'example content';
 777          $contenthash = file_storage::hash_from_string($filecontent);
 778          $filedircontent = [
 779              '0f' => [
 780                  'f3' => [
 781                      // This contains a virtual file which has a cache mismatch.
 782                      '0ff30941ca5acd879fd809e8c937d9f9e6dd1615' => 'different example content',
 783                  ],
 784              ],
 785          ];
 786          $sourcedir = [
 787              'file' => $filecontent,
 788          ];
 789          $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
 790  
 791          // Check that we hit the jackpot.
 792          $fs = new file_system_filedir();
 793          $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
 794          $result = $fs->add_file_from_path($filepath);
 795  
 796          // We provided a bad hash. Check that the file was replaced.
 797          $this->assertDebuggingCalled("Replacing invalid content file $contenthash");
 798  
 799          // Test the output.
 800          $this->assertEquals($contenthash, $result[0]);
 801          $this->assertEquals(core_text::strlen($filecontent), $result[1]);
 802          $this->assertFalse($result[2]);
 803  
 804          // Fetch the new file structure.
 805          $structure = \org\bovigo\vfs\vfsStream::inspect(
 806              new \org\bovigo\vfs\visitor\vfsStreamStructureVisitor()
 807          )->getStructure();
 808  
 809          $this->assertEquals($filecontent, $structure['root']['filedir']['0f']['f3'][$contenthash]);
 810      }
 811  
 812      /**
 813       * Test that an appropriate error message is generated when adding a
 814       * file to the pool when the pool directory structure is not writable.
 815       *
 816       * @covers ::add_file_from_path
 817       */
 818      public function test_add_file_from_path_existing_cannot_write_hashpath() {
 819          $this->resetAfterTest();
 820  
 821          $filecontent = 'example content';
 822          $contenthash = file_storage::hash_from_string($filecontent);
 823          $filedircontent = [
 824              '0f' => [],
 825          ];
 826          $sourcedir = [
 827              'file' => $filecontent,
 828          ];
 829          $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
 830  
 831          // Make the target path readonly.
 832          $vfileroot->getChild('filedir/0f')
 833              ->chmod(0444)
 834              ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 835  
 836          $this->expectException('file_exception');
 837          $this->expectExceptionMessageRegExp(
 838              "/Cannot create local file pool directories. Please verify permissions in dataroot./");
 839  
 840          // Attempt to add the file to the file pool.
 841          $fs = new file_system_filedir();
 842          $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
 843          $fs->add_file_from_path($sourcefile);
 844      }
 845  
 846      /**
 847       * Test adding a string to the pool.
 848       *
 849       * @covers ::add_file_from_string
 850       */
 851      public function test_add_file_from_string() {
 852          $this->resetAfterTest();
 853          global $CFG;
 854  
 855          $filecontent = 'example content';
 856          $contenthash = file_storage::hash_from_string($filecontent);
 857          $vfileroot = $this->setup_vfile_root();
 858  
 859          // Note, the vfs file system does not support locks - prevent file locking here.
 860          $CFG->preventfilelocking = true;
 861  
 862          // Attempt to add the file to the file pool.
 863          $fs = new file_system_filedir();
 864          $result = $fs->add_file_from_string($filecontent);
 865  
 866          // Test the output.
 867          $this->assertEquals($contenthash, $result[0]);
 868          $this->assertEquals(core_text::strlen($filecontent), $result[1]);
 869          $this->assertTrue($result[2]);
 870      }
 871  
 872      /**
 873       * Test that an appropriate error message is generated when adding a
 874       * string to the pool when the pool directory structure is not writable.
 875       *
 876       * @covers ::add_file_from_string
 877       */
 878      public function test_add_file_from_string_existing_cannot_write_hashpath() {
 879          $this->resetAfterTest();
 880  
 881          $filecontent = 'example content';
 882          $contenthash = file_storage::hash_from_string($filecontent);
 883  
 884          $filedircontent = [
 885              '0f' => [],
 886          ];
 887          $vfileroot = $this->setup_vfile_root($filedircontent);
 888  
 889          // Make the target path readonly.
 890          $vfileroot->getChild('filedir/0f')
 891              ->chmod(0444)
 892              ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
 893  
 894          $this->expectException('file_exception');
 895          $this->expectExceptionMessageRegExp(
 896              "/Cannot create local file pool directories. Please verify permissions in dataroot./");
 897  
 898          // Attempt to add the file to the file pool.
 899          $fs = new file_system_filedir();
 900          $fs->add_file_from_string($filecontent);
 901      }
 902  
 903      /**
 904       * Test adding a string to the pool when an item with the same
 905       * contenthash is already present.
 906       *
 907       * @covers ::add_file_from_string
 908       */
 909      public function test_add_file_from_string_existing_matches() {
 910          $this->resetAfterTest();
 911          global $CFG;
 912  
 913          $filecontent = 'example content';
 914          $contenthash = file_storage::hash_from_string($filecontent);
 915          $filedircontent = [
 916              '0f' => [
 917                  'f3' => [
 918                      $contenthash => $filecontent,
 919                  ],
 920              ],
 921          ];
 922  
 923          $vfileroot = $this->setup_vfile_root($filedircontent);
 924  
 925          // Note, the vfs file system does not support locks - prevent file locking here.
 926          $CFG->preventfilelocking = true;
 927  
 928          // Attempt to add the file to the file pool.
 929          $fs = new file_system_filedir();
 930          $result = $fs->add_file_from_string($filecontent);
 931  
 932          // Test the output.
 933          $this->assertEquals($contenthash, $result[0]);
 934          $this->assertEquals(core_text::strlen($filecontent), $result[1]);
 935          $this->assertFalse($result[2]);
 936      }
 937  
 938      /**
 939       * Test the cleanup of deleted files when there are no files to delete.
 940       *
 941       * @covers ::remove_file
 942       */
 943      public function test_remove_file_missing() {
 944          $this->resetAfterTest();
 945  
 946          $filecontent = 'example content';
 947          $contenthash = file_storage::hash_from_string($filecontent);
 948          $vfileroot = $this->setup_vfile_root();
 949  
 950          $fs = new file_system_filedir();
 951          $fs->remove_file($contenthash);
 952  
 953          $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
 954          // No file to move to trash, so the trash path will also be empty.
 955          $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
 956          $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
 957          $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
 958      }
 959  
 960      /**
 961       * Test the cleanup of deleted files when a file already exists in the
 962       * trash for that path.
 963       *
 964       * @covers ::remove_file
 965       */
 966      public function test_remove_file_existing_trash() {
 967          $this->resetAfterTest();
 968  
 969          $filecontent = 'example content';
 970          $contenthash = file_storage::hash_from_string($filecontent);
 971  
 972          $filedircontent = $trashdircontent = [
 973              '0f' => [
 974                  'f3' => [
 975                      $contenthash => $filecontent,
 976                  ],
 977              ],
 978          ];
 979          $trashdircontent['0f']['f3'][$contenthash] .= 'different';
 980          $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
 981  
 982          $fs = new file_system_filedir();
 983          $fs->remove_file($contenthash);
 984  
 985          $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
 986          $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
 987          $this->assertNotEquals($filecontent, $vfileroot->getChild('trashdir/0f/f3/' . $contenthash)->getContent());
 988      }
 989  
 990      /**
 991       * Ensure that remove_file does nothing with an empty file.
 992       *
 993       * @covers ::remove_file
 994       */
 995      public function test_remove_file_empty() {
 996          $this->resetAfterTest();
 997          global $DB;
 998  
 999          $DB = $this->getMockBuilder(\moodle_database::class)
1000              ->setMethods(['record_exists'])
1001              ->getMockForAbstractClass();
1002  
1003          $DB->expects($this->never())
1004              ->method('record_exists');
1005  
1006          $fs = new file_system_filedir();
1007  
1008          $result = $fs->remove_file(file_storage::hash_from_string(''));
1009          $this->assertNull($result);
1010      }
1011  
1012      /**
1013       * Ensure that remove_file does nothing when a file is still
1014       * in use.
1015       *
1016       * @covers ::remove_file
1017       */
1018      public function test_remove_file_in_use() {
1019          $this->resetAfterTest();
1020          global $DB;
1021  
1022          $filecontent = 'example content';
1023          $contenthash = file_storage::hash_from_string($filecontent);
1024          $filedircontent = [
1025              '0f' => [
1026                  'f3' => [
1027                      $contenthash => $filecontent,
1028                  ],
1029              ],
1030          ];
1031          $vfileroot = $this->setup_vfile_root($filedircontent);
1032  
1033          $DB = $this->getMockBuilder(\moodle_database::class)
1034              ->setMethods(['record_exists'])
1035              ->getMockForAbstractClass();
1036  
1037          $DB->method('record_exists')->willReturn(true);
1038  
1039          $fs = new file_system_filedir();
1040          $result = $fs->remove_file($contenthash);
1041          $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1042          $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1043      }
1044  
1045      /**
1046       * Ensure that remove_file removes the file when it is no
1047       * longer in use.
1048       *
1049       * @covers ::remove_file
1050       */
1051      public function test_remove_file_expired() {
1052          $this->resetAfterTest();
1053          global $DB;
1054  
1055          $filecontent = 'example content';
1056          $contenthash = file_storage::hash_from_string($filecontent);
1057          $filedircontent = [
1058              '0f' => [
1059                  'f3' => [
1060                      $contenthash => $filecontent,
1061                  ],
1062              ],
1063          ];
1064          $vfileroot = $this->setup_vfile_root($filedircontent);
1065  
1066          $DB = $this->getMockBuilder(\moodle_database::class)
1067              ->setMethods(['record_exists'])
1068              ->getMockForAbstractClass();
1069  
1070          $DB->method('record_exists')->willReturn(false);
1071  
1072          $fs = new file_system_filedir();
1073          $result = $fs->remove_file($contenthash);
1074          $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1075          $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1076      }
1077  
1078      /**
1079       * Test purging the cache.
1080       *
1081       * @covers ::empty_trash
1082       */
1083      public function test_empty_trash() {
1084          $this->resetAfterTest();
1085  
1086          $filecontent = 'example content';
1087          $contenthash = file_storage::hash_from_string($filecontent);
1088  
1089          $filedircontent = $trashdircontent = [
1090              '0f' => [
1091                  'f3' => [
1092                      $contenthash => $filecontent,
1093                  ],
1094              ],
1095          ];
1096          $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
1097  
1098          $fs = new file_system_filedir();
1099          $method = new ReflectionMethod(file_system_filedir::class, 'empty_trash');
1100          $method->setAccessible(true);
1101          $result = $method->invoke($fs);
1102  
1103          $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1104          $this->assertFalse($vfileroot->hasChild('trashdir'));
1105          $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
1106          $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
1107          $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1108      }
1109  
1110      /**
1111       * Data Provider for contenthash to contendir conversion.
1112       *
1113       * @return  array
1114       */
1115      public function contenthash_dataprovider() {
1116          return array(
1117              array(
1118                  'contenthash'   => 'eee4943847a35a4b6942c6f96daafde06bcfdfab',
1119                  'contentdir'    => 'ee/e4',
1120              ),
1121              array(
1122                  'contenthash'   => 'aef05a62ae81ca0005d2569447779af062b7cda0',
1123                  'contentdir'    => 'ae/f0',
1124              ),
1125          );
1126      }
1127  }