Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [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  /**
  18   * Core file system class definition.
  19   *
  20   * @package   core_files
  21   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * File system class used for low level access to real files in filedir.
  29   *
  30   * @package   core_files
  31   * @category  files
  32   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  abstract class file_system {
  36  
  37      /**
  38       * Output the content of the specified stored file.
  39       *
  40       * Note, this is different to get_content() as it uses the built-in php
  41       * readfile function which is more efficient.
  42       *
  43       * @param stored_file $file The file to serve.
  44       * @return void
  45       */
  46      public function readfile(stored_file $file) {
  47          if ($this->is_file_readable_locally_by_storedfile($file, false)) {
  48              $path = $this->get_local_path_from_storedfile($file, false);
  49          } else {
  50              $path = $this->get_remote_path_from_storedfile($file);
  51          }
  52          if (readfile_allow_large($path, $file->get_filesize()) === false) {
  53              throw new file_exception('storedfilecannotreadfile', $file->get_filename());
  54          }
  55      }
  56  
  57      /**
  58       * Get the full path on disk for the specified stored file.
  59       *
  60       * Note: This must return a consistent path for the file's contenthash
  61       * and the path _will_ be in a standard local format.
  62       * Streamable paths will not work.
  63       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
  64       *
  65       * The $fetchifnotfound allows you to determine the expected path of the file.
  66       *
  67       * @param stored_file $file The file to serve.
  68       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
  69       * @return string full path to pool file with file content
  70       */
  71      public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) {
  72          return $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound);
  73      }
  74  
  75      /**
  76       * Get a remote filepath for the specified stored file.
  77       *
  78       * This is typically either the same as the local filepath, or it is a streamable resource.
  79       *
  80       * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
  81       *
  82       * @param stored_file $file The file to serve.
  83       * @return string full path to pool file with file content
  84       */
  85      public function get_remote_path_from_storedfile(stored_file $file) {
  86          return $this->get_remote_path_from_hash($file->get_contenthash(), false);
  87      }
  88  
  89      /**
  90       * Get the full path for the specified hash, including the path to the filedir.
  91       *
  92       * Note: This must return a consistent path for the file's contenthash
  93       * and the path _will_ be in a standard local format.
  94       * Streamable paths will not work.
  95       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
  96       *
  97       * The $fetchifnotfound allows you to determine the expected path of the file.
  98       *
  99       * @param string $contenthash The content hash
 100       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 101       * @return string The full path to the content file
 102       */
 103      abstract protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false);
 104  
 105      /**
 106       * Get the full path for the specified hash, including the path to the filedir.
 107       *
 108       * This is typically either the same as the local filepath, or it is a streamable resource.
 109       *
 110       * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
 111       *
 112       * @param string $contenthash The content hash
 113       * @return string The full path to the content file
 114       */
 115      abstract protected function get_remote_path_from_hash($contenthash);
 116  
 117      /**
 118       * Determine whether the file is present on the file system somewhere.
 119       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
 120       *
 121       * The $fetchifnotfound allows you to determine the expected path of the file.
 122       *
 123       * @param stored_file $file The file to ensure is available.
 124       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 125       * @return bool
 126       */
 127      public function is_file_readable_locally_by_storedfile(stored_file $file, $fetchifnotfound = false) {
 128          if (!$file->get_filesize()) {
 129              // Files with empty size are either directories or empty.
 130              // We handle these virtually.
 131              return true;
 132          }
 133  
 134          // Check to see if the file is currently readable.
 135          $path = $this->get_local_path_from_storedfile($file, $fetchifnotfound);
 136          if (is_readable($path)) {
 137              return true;
 138          }
 139  
 140          return false;
 141      }
 142  
 143      /**
 144       * Determine whether the file is present on the local file system somewhere.
 145       *
 146       * @param stored_file $file The file to ensure is available.
 147       * @return bool
 148       */
 149      public function is_file_readable_remotely_by_storedfile(stored_file $file) {
 150          if (!$file->get_filesize()) {
 151              // Files with empty size are either directories or empty.
 152              // We handle these virtually.
 153              return true;
 154          }
 155  
 156          $path = $this->get_remote_path_from_storedfile($file, false);
 157          if (is_readable($path)) {
 158              return true;
 159          }
 160  
 161          return false;
 162      }
 163  
 164      /**
 165       * Determine whether the file is present on the file system somewhere given
 166       * the contenthash.
 167       *
 168       * @param string $contenthash The contenthash of the file to check.
 169       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 170       * @return bool
 171       */
 172      public function is_file_readable_locally_by_hash($contenthash, $fetchifnotfound = false) {
 173          if ($contenthash === file_storage::hash_from_string('')) {
 174              // Files with empty size are either directories or empty.
 175              // We handle these virtually.
 176              return true;
 177          }
 178  
 179          // This is called by file_storage::content_exists(), and in turn by the repository system.
 180          $path = $this->get_local_path_from_hash($contenthash, $fetchifnotfound);
 181  
 182          // Note - it is not possible to perform a content recovery safely from a hash alone.
 183          return is_readable($path);
 184      }
 185  
 186      /**
 187       * Determine whether the file is present locally on the file system somewhere given
 188       * the contenthash.
 189       *
 190       * @param string $contenthash The contenthash of the file to check.
 191       * @return bool
 192       */
 193      public function is_file_readable_remotely_by_hash($contenthash) {
 194          if ($contenthash === file_storage::hash_from_string('')) {
 195              // Files with empty size are either directories or empty.
 196              // We handle these virtually.
 197              return true;
 198          }
 199  
 200          $path = $this->get_remote_path_from_hash($contenthash, false);
 201  
 202          // Note - it is not possible to perform a content recovery safely from a hash alone.
 203          return is_readable($path);
 204      }
 205  
 206      /**
 207       * Copy content of file to given pathname.
 208       *
 209       * @param stored_file $file The file to be copied
 210       * @param string $target real path to the new file
 211       * @return bool success
 212       */
 213      abstract public function copy_content_from_storedfile(stored_file $file, $target);
 214  
 215      /**
 216       * Remove the file with the specified contenthash.
 217       *
 218       * Note, if overriding this function, you _must_ check that the file is
 219       * no longer in use - see {check_file_usage}.
 220       *
 221       * DO NOT call directly - reserved for core!!
 222       *
 223       * @param string $contenthash
 224       */
 225      abstract public function remove_file($contenthash);
 226  
 227      /**
 228       * Check whether a file is removable.
 229       *
 230       * This must be called prior to file removal.
 231       *
 232       * @param string $contenthash
 233       * @return bool
 234       */
 235      protected static function is_file_removable($contenthash) {
 236          global $DB;
 237  
 238          if ($contenthash === file_storage::hash_from_string('')) {
 239              // No need to delete files without content.
 240              return false;
 241          }
 242  
 243          // Note: This section is critical - in theory file could be reused at the same time, if this
 244          // happens we can still recover the file from trash.
 245          // Technically this is the responsibility of the file_storage API, but as this method is public, we go belt-and-braces.
 246          if ($DB->record_exists('files', array('contenthash' => $contenthash))) {
 247              // File content is still used.
 248              return false;
 249          }
 250  
 251          return true;
 252      }
 253  
 254      /**
 255       * Get the content of the specified stored file.
 256       *
 257       * Generally you will probably want to use readfile() to serve content,
 258       * and where possible you should see if you can use
 259       * get_content_file_handle and work with the file stream instead.
 260       *
 261       * @param stored_file $file The file to retrieve
 262       * @return string The full file content
 263       */
 264      public function get_content(stored_file $file) {
 265          if (!$file->get_filesize()) {
 266              // Directories are empty. Empty files are not worth fetching.
 267              return '';
 268          }
 269  
 270          $source = $this->get_remote_path_from_storedfile($file);
 271          return file_get_contents($source);
 272      }
 273  
 274      /**
 275       * List contents of archive.
 276       *
 277       * @param stored_file $file The archive to inspect
 278       * @param file_packer $packer file packer instance
 279       * @return array of file infos
 280       */
 281      public function list_files($file, file_packer $packer) {
 282          $archivefile = $this->get_local_path_from_storedfile($file, true);
 283          return $packer->list_files($archivefile);
 284      }
 285  
 286      /**
 287       * Extract file to given file path (real OS filesystem), existing files are overwritten.
 288       *
 289       * @param stored_file $file The archive to inspect
 290       * @param file_packer $packer File packer instance
 291       * @param string $pathname Target directory
 292       * @param file_progress $progress progress indicator callback or null if not required
 293       * @return array|bool List of processed files; false if error
 294       */
 295      public function extract_to_pathname(stored_file $file, file_packer $packer, $pathname, file_progress $progress = null) {
 296          $archivefile = $this->get_local_path_from_storedfile($file, true);
 297          return $packer->extract_to_pathname($archivefile, $pathname, null, $progress);
 298      }
 299  
 300      /**
 301       * Extract file to given file path (real OS filesystem), existing files are overwritten.
 302       *
 303       * @param stored_file $file The archive to inspect
 304       * @param file_packer $packer file packer instance
 305       * @param int $contextid context ID
 306       * @param string $component component
 307       * @param string $filearea file area
 308       * @param int $itemid item ID
 309       * @param string $pathbase path base
 310       * @param int $userid user ID
 311       * @param file_progress $progress Progress indicator callback or null if not required
 312       * @return array|bool list of processed files; false if error
 313       */
 314      public function extract_to_storage(stored_file $file, file_packer $packer, $contextid,
 315              $component, $filearea, $itemid, $pathbase, $userid = null, file_progress $progress = null) {
 316  
 317          // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
 318          $archivefile = $this->get_local_path_from_storedfile($file, true);
 319          return $packer->extract_to_storage($archivefile, $contextid,
 320                  $component, $filearea, $itemid, $pathbase, $userid, $progress);
 321      }
 322  
 323      /**
 324       * Add file/directory into archive.
 325       *
 326       * @param stored_file $file The file to archive
 327       * @param file_archive $filearch file archive instance
 328       * @param string $archivepath pathname in archive
 329       * @return bool success
 330       */
 331      public function add_storedfile_to_archive(stored_file $file, file_archive $filearch, $archivepath) {
 332          if ($file->is_directory()) {
 333              return $filearch->add_directory($archivepath);
 334          } else {
 335              // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
 336              return $filearch->add_file_from_pathname($archivepath, $this->get_local_path_from_storedfile($file, true));
 337          }
 338      }
 339  
 340      /**
 341       * Adds this file path to a curl request (POST only).
 342       *
 343       * @param stored_file $file The file to add to the curl request
 344       * @param curl $curlrequest The curl request object
 345       * @param string $key What key to use in the POST request
 346       * @return void
 347       * This needs the fullpath for the storedfile :/
 348       * Can this be achieved in some other fashion?
 349       */
 350      public function add_to_curl_request(stored_file $file, &$curlrequest, $key) {
 351          // Note: curl_file_create does not work with remote paths.
 352          $path = $this->get_local_path_from_storedfile($file, true);
 353          $curlrequest->_tmp_file_post_params[$key] = curl_file_create($path, null, $file->get_filename());
 354      }
 355  
 356      /**
 357       * Returns information about image.
 358       * Information is determined from the file content
 359       *
 360       * @param stored_file $file The file to inspect
 361       * @return mixed array with width, height and mimetype; false if not an image
 362       */
 363      public function get_imageinfo(stored_file $file) {
 364          if (!$this->is_image_from_storedfile($file)) {
 365              return false;
 366          }
 367  
 368          $hash = $file->get_contenthash();
 369          $cache = cache::make('core', 'file_imageinfo');
 370          $info = $cache->get($hash);
 371          if ($info !== false) {
 372              return $info;
 373          }
 374  
 375          // Whilst get_imageinfo_from_path can use remote paths, it must download the entire file first.
 376          // It is more efficient to use a local file when possible.
 377          $info = $this->get_imageinfo_from_path($this->get_local_path_from_storedfile($file, true));
 378          $cache->set($hash, $info);
 379          return $info;
 380      }
 381  
 382      /**
 383       * Attempt to determine whether the specified file is likely to be an
 384       * image.
 385       * Since this relies upon the mimetype stored in the files table, there
 386       * may be times when this information is not 100% accurate.
 387       *
 388       * @param stored_file $file The file to check
 389       * @return bool
 390       */
 391      public function is_image_from_storedfile(stored_file $file) {
 392          if (!$file->get_filesize()) {
 393              // An empty file cannot be an image.
 394              return false;
 395          }
 396  
 397          $mimetype = $file->get_mimetype();
 398          if (!preg_match('|^image/|', $mimetype)) {
 399              // The mimetype does not include image.
 400              return false;
 401          }
 402  
 403          // If it looks like an image, and it smells like an image, perhaps it's an image!
 404          return true;
 405      }
 406  
 407      /**
 408       * Returns image information relating to the specified path or URL.
 409       *
 410       * @param string $path The full path of the image file.
 411       * @return array|bool array that containing width, height, and mimetype or false if cannot get the image info.
 412       */
 413      protected function get_imageinfo_from_path($path) {
 414          $imagemimetype = file_storage::mimetype_from_file($path);
 415          $issvgimage = file_is_svg_image_from_mimetype($imagemimetype);
 416  
 417          if (!$issvgimage) {
 418              $imageinfo = getimagesize($path);
 419              if (!is_array($imageinfo)) {
 420                  return false; // Nothing to process, the file was not recognised as image by GD.
 421              }
 422              $image = [
 423                      'width' => $imageinfo[0],
 424                      'height' => $imageinfo[1],
 425                      'mimetype' => image_type_to_mime_type($imageinfo[2]),
 426              ];
 427          } else {
 428              // Since SVG file is actually an XML file, GD cannot handle.
 429              $svgcontent = @simplexml_load_file($path);
 430              if (!$svgcontent) {
 431                  // Cannot parse the file.
 432                  return false;
 433              }
 434              $svgattrs = $svgcontent->attributes();
 435  
 436              if (!empty($svgattrs->viewBox)) {
 437                  // We have viewBox.
 438                  $viewboxval = explode(' ', $svgattrs->viewBox);
 439                  $width = intval($viewboxval[2]);
 440                  $height = intval($viewboxval[3]);
 441              } else {
 442                  // Get the width.
 443                  if (!empty($svgattrs->width) && intval($svgattrs->width) > 0) {
 444                      $width = intval($svgattrs->width);
 445                  } else {
 446                      // Default width.
 447                      $width = 800;
 448                  }
 449                  // Get the height.
 450                  if (!empty($svgattrs->height) && intval($svgattrs->height) > 0) {
 451                      $height = intval($svgattrs->height);
 452                  } else {
 453                      // Default width.
 454                      $height = 600;
 455                  }
 456              }
 457  
 458              $image = [
 459                      'width' => $width,
 460                      'height' => $height,
 461                      'mimetype' => $imagemimetype,
 462              ];
 463          }
 464  
 465          if (empty($image['width']) or empty($image['height']) or empty($image['mimetype'])) {
 466              // GD can not parse it, sorry.
 467              return false;
 468          }
 469          return $image;
 470      }
 471  
 472      /**
 473       * Serve file content using X-Sendfile header.
 474       * Please make sure that all headers are already sent and the all
 475       * access control checks passed.
 476       *
 477       * This alternate method to xsendfile() allows an alternate file system
 478       * to use the full file metadata and avoid extra lookups.
 479       *
 480       * @param stored_file $file The file to send
 481       * @return bool success
 482       */
 483      public function xsendfile_file(stored_file $file): bool {
 484          return $this->xsendfile($file->get_contenthash());
 485      }
 486  
 487      /**
 488       * Serve file content using X-Sendfile header.
 489       * Please make sure that all headers are already sent and the all
 490       * access control checks passed.
 491       *
 492       * @param string $contenthash The content hash of the file to be served
 493       * @return bool success
 494       */
 495      public function xsendfile($contenthash) {
 496          global $CFG;
 497          require_once($CFG->libdir . "/xsendfilelib.php");
 498  
 499          return xsendfile($this->get_remote_path_from_hash($contenthash));
 500      }
 501  
 502      /**
 503       * Returns true if filesystem is configured to support xsendfile.
 504       *
 505       * @return bool
 506       */
 507      public function supports_xsendfile() {
 508          global $CFG;
 509          return !empty($CFG->xsendfile);
 510      }
 511  
 512      /**
 513       * Validate that the content hash matches the content hash of the file on disk.
 514       *
 515       * @param string $contenthash The current content hash to validate
 516       * @param string $pathname The path to the file on disk
 517       * @return array The content hash (it might change) and file size
 518       */
 519      protected function validate_hash_and_file_size($contenthash, $pathname) {
 520          global $CFG;
 521  
 522          if (!is_readable($pathname)) {
 523              throw new file_exception('storedfilecannotread', '', $pathname);
 524          }
 525  
 526          $filesize = filesize($pathname);
 527          if ($filesize === false) {
 528              throw new file_exception('storedfilecannotread', '', $pathname);
 529          }
 530  
 531          if (is_null($contenthash)) {
 532              $contenthash = file_storage::hash_from_path($pathname);
 533          } else if ($CFG->debugdeveloper) {
 534              $filehash = file_storage::hash_from_path($pathname);
 535              if ($filehash === false) {
 536                  throw new file_exception('storedfilecannotread', '', $pathname);
 537              }
 538              if ($filehash !== $contenthash) {
 539                  // Hopefully this never happens, if yes we need to fix calling code.
 540                  debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
 541                  $contenthash = $filehash;
 542              }
 543          }
 544          if ($contenthash === false) {
 545              throw new file_exception('storedfilecannotread', '', $pathname);
 546          }
 547  
 548          if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
 549              // Did the file change or is file_storage::hash_from_path() borked for this file?
 550              clearstatcache();
 551              $contenthash = file_storage::hash_from_path($pathname);
 552              $filesize    = filesize($pathname);
 553  
 554              if ($contenthash === false or $filesize === false) {
 555                  throw new file_exception('storedfilecannotread', '', $pathname);
 556              }
 557              if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
 558                  // This is very weird...
 559                  throw new file_exception('storedfilecannotread', '', $pathname);
 560              }
 561          }
 562  
 563          return [$contenthash, $filesize];
 564      }
 565  
 566      /**
 567       * Add the supplied file to the file system.
 568       *
 569       * Note: If overriding this function, it is advisable to store the file
 570       * in the path returned by get_local_path_from_hash as there may be
 571       * subsequent uses of the file in the same request.
 572       *
 573       * @param string $pathname Path to file currently on disk
 574       * @param string $contenthash SHA1 hash of content if known (performance only)
 575       * @return array (contenthash, filesize, newfile)
 576       */
 577      abstract public function add_file_from_path($pathname, $contenthash = null);
 578  
 579      /**
 580       * Add a file with the supplied content to the file system.
 581       *
 582       * Note: If overriding this function, it is advisable to store the file
 583       * in the path returned by get_local_path_from_hash as there may be
 584       * subsequent uses of the file in the same request.
 585       *
 586       * @param string $content file content - binary string
 587       * @return array (contenthash, filesize, newfile)
 588       */
 589      abstract public function add_file_from_string($content);
 590  
 591      /**
 592       * Returns file handle - read only mode, no writing allowed into pool files!
 593       *
 594       * When you want to modify a file, create a new file and delete the old one.
 595       *
 596       * @param stored_file $file The file to retrieve a handle for
 597       * @param int $type Type of file handle (FILE_HANDLE_xx constant)
 598       * @return resource file handle
 599       */
 600      public function get_content_file_handle(stored_file $file, $type = stored_file::FILE_HANDLE_FOPEN) {
 601          if ($type === stored_file::FILE_HANDLE_GZOPEN) {
 602              // Local file required for gzopen.
 603              $path = $this->get_local_path_from_storedfile($file, true);
 604          } else {
 605              $path = $this->get_remote_path_from_storedfile($file);
 606          }
 607  
 608          return self::get_file_handle_for_path($path, $type);
 609      }
 610  
 611      /**
 612       * Return a file handle for the specified path.
 613       *
 614       * This abstraction should be used when overriding get_content_file_handle in a new file system.
 615       *
 616       * @param string $path The path to the file. This shoudl be any type of path that fopen and gzopen accept.
 617       * @param int $type Type of file handle (FILE_HANDLE_xx constant)
 618       * @return resource
 619       * @throws coding_exception When an unexpected type of file handle is requested
 620       */
 621      protected static function get_file_handle_for_path($path, $type = stored_file::FILE_HANDLE_FOPEN) {
 622          switch ($type) {
 623              case stored_file::FILE_HANDLE_FOPEN:
 624                  // Binary reading.
 625                  return fopen($path, 'rb');
 626              case stored_file::FILE_HANDLE_GZOPEN:
 627                  // Binary reading of file in gz format.
 628                  return gzopen($path, 'rb');
 629              default:
 630                  throw new coding_exception('Unexpected file handle type');
 631          }
 632      }
 633  
 634      /**
 635       * Retrieve the mime information for the specified stored file.
 636       *
 637       * @param string $contenthash
 638       * @param string $filename
 639       * @return string The MIME type.
 640       */
 641      public function mimetype_from_hash($contenthash, $filename) {
 642          $pathname = $this->get_local_path_from_hash($contenthash);
 643          $mimetype = file_storage::mimetype($pathname, $filename);
 644  
 645          if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) {
 646              // The type is unknown, but the full checks weren't completed because the file isn't locally available.
 647              // Ensure we have a local copy and try again.
 648              $pathname = $this->get_local_path_from_hash($contenthash, true);
 649              $mimetype = file_storage::mimetype_from_file($pathname);
 650          }
 651  
 652          return $mimetype;
 653      }
 654  
 655      /**
 656       * Retrieve the mime information for the specified stored file.
 657       *
 658       * @param stored_file $file The stored file to retrieve mime information for
 659       * @return string The MIME type.
 660       */
 661      public function mimetype_from_storedfile($file) {
 662          if (!$file->get_filesize()) {
 663              // Files with an empty filesize are treated as directories and have no mimetype.
 664              return null;
 665          }
 666          return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename());
 667      }
 668  
 669      /**
 670       * Run any periodic tasks which must be performed.
 671       */
 672      public function cron() {
 673      }
 674  }