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  /**
  19   * Core file storage class definition.
  20   *
  21   * @package   core_files
  22   * @copyright 2008 Petr Skoda {@link http://skodak.org}
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once("$CFG->libdir/filestorage/stored_file.php");
  29  
  30  /**
  31   * File storage class used for low level access to stored files.
  32   *
  33   * Only owner of file area may use this class to access own files,
  34   * for example only code in mod/assignment/* may access assignment
  35   * attachments. When some other part of moodle needs to access
  36   * files of modules it has to use file_browser class instead or there
  37   * has to be some callback API.
  38   *
  39   * @package   core_files
  40   * @category  files
  41   * @copyright 2008 Petr Skoda {@link http://skodak.org}
  42   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   * @since     Moodle 2.0
  44   */
  45  class file_storage {
  46  
  47      /** @var string tempdir */
  48      private $tempdir;
  49  
  50      /** @var file_system filesystem */
  51      private $filesystem;
  52  
  53      /**
  54       * Constructor - do not use directly use {@link get_file_storage()} call instead.
  55       */
  56      public function __construct() {
  57          // The tempdir must always remain on disk, but shared between all ndoes in a cluster. Its content is not subject
  58          // to the file_system abstraction.
  59          $this->tempdir = make_temp_directory('filestorage');
  60  
  61          $this->setup_file_system();
  62      }
  63  
  64      /**
  65       * Complete setup procedure for the file_system component.
  66       *
  67       * @return file_system
  68       */
  69      public function setup_file_system() {
  70          global $CFG;
  71          if ($this->filesystem === null) {
  72              require_once($CFG->libdir . '/filestorage/file_system.php');
  73              if (!empty($CFG->alternative_file_system_class)) {
  74                  $class = $CFG->alternative_file_system_class;
  75              } else {
  76                  // The default file_system is the filedir.
  77                  require_once($CFG->libdir . '/filestorage/file_system_filedir.php');
  78                  $class = file_system_filedir::class;
  79              }
  80              $this->filesystem = new $class();
  81          }
  82  
  83          return $this->filesystem;
  84      }
  85  
  86      /**
  87       * Return the file system instance.
  88       *
  89       * @return file_system
  90       */
  91      public function get_file_system() {
  92          return $this->filesystem;
  93      }
  94  
  95      /**
  96       * Calculates sha1 hash of unique full path name information.
  97       *
  98       * This hash is a unique file identifier - it is used to improve
  99       * performance and overcome db index size limits.
 100       *
 101       * @param int $contextid context ID
 102       * @param string $component component
 103       * @param string $filearea file area
 104       * @param int $itemid item ID
 105       * @param string $filepath file path
 106       * @param string $filename file name
 107       * @return string sha1 hash
 108       */
 109      public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
 110          if (substr($filepath, 0, 1) != '/') {
 111              $filepath = '/' . $filepath;
 112          }
 113          if (substr($filepath, - 1) != '/') {
 114              $filepath .= '/';
 115          }
 116          return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
 117      }
 118  
 119      /**
 120       * Does this file exist?
 121       *
 122       * @param int $contextid context ID
 123       * @param string $component component
 124       * @param string $filearea file area
 125       * @param int $itemid item ID
 126       * @param string $filepath file path
 127       * @param string $filename file name
 128       * @return bool
 129       */
 130      public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
 131          $filepath = clean_param($filepath, PARAM_PATH);
 132          $filename = clean_param($filename, PARAM_FILE);
 133  
 134          if ($filename === '') {
 135              $filename = '.';
 136          }
 137  
 138          $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
 139          return $this->file_exists_by_hash($pathnamehash);
 140      }
 141  
 142      /**
 143       * Whether or not the file exist
 144       *
 145       * @param string $pathnamehash path name hash
 146       * @return bool
 147       */
 148      public function file_exists_by_hash($pathnamehash) {
 149          global $DB;
 150  
 151          return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
 152      }
 153  
 154      /**
 155       * Create instance of file class from database record.
 156       *
 157       * @param stdClass $filerecord record from the files table left join files_reference table
 158       * @return stored_file instance of file abstraction class
 159       */
 160      public function get_file_instance(stdClass $filerecord) {
 161          $storedfile = new stored_file($this, $filerecord);
 162          return $storedfile;
 163      }
 164  
 165      /**
 166       * Get converted document.
 167       *
 168       * Get an alternate version of the specified document, if it is possible to convert.
 169       *
 170       * @param stored_file $file the file we want to preview
 171       * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
 172       * @param boolean $forcerefresh If true, the file will be converted every time (not cached).
 173       * @return stored_file|bool false if unable to create the conversion, stored file otherwise
 174       */
 175      public function get_converted_document(stored_file $file, $format, $forcerefresh = false) {
 176          debugging('The get_converted_document function has been deprecated and the unoconv functions been removed. '
 177                  . 'The file has not been converted. '
 178                  . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
 179  
 180          return false;
 181      }
 182  
 183      /**
 184       * Verify the format is supported.
 185       *
 186       * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
 187       * @return bool - True if the format is supported for input.
 188       */
 189      protected function is_format_supported_by_unoconv($format) {
 190          debugging('The is_format_supported_by_unoconv function has been deprecated and the unoconv functions been removed. '
 191                  . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
 192  
 193          return false;
 194      }
 195  
 196      /**
 197       * Check if the installed version of unoconv is supported.
 198       *
 199       * @return bool true if the present version is supported, false otherwise.
 200       */
 201      public static function can_convert_documents() {
 202          debugging('The can_convert_documents function has been deprecated and the unoconv functions been removed. '
 203                  . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
 204  
 205          return false;
 206      }
 207  
 208      /**
 209       * Regenerate the test pdf and send it direct to the browser.
 210       */
 211      public static function send_test_pdf() {
 212          debugging('The send_test_pdf function has been deprecated and the unoconv functions been removed. '
 213                  . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
 214  
 215          return false;
 216      }
 217  
 218      /**
 219       * Check if unoconv configured path is correct and working.
 220       *
 221       * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
 222       */
 223      public static function test_unoconv_path() {
 224          debugging('The test_unoconv_path function has been deprecated and the unoconv functions been removed. '
 225                  . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
 226  
 227          return false;
 228      }
 229  
 230      /**
 231       * Returns an image file that represent the given stored file as a preview
 232       *
 233       * At the moment, only GIF, JPEG, PNG and SVG files are supported to have previews. In the
 234       * future, the support for other mimetypes can be added, too (eg. generate an image
 235       * preview of PDF, text documents etc).
 236       *
 237       * @param stored_file $file the file we want to preview
 238       * @param string $mode preview mode, eg. 'thumb'
 239       * @return stored_file|bool false if unable to create the preview, stored file otherwise
 240       */
 241      public function get_file_preview(stored_file $file, $mode) {
 242  
 243          $context = context_system::instance();
 244          $path = '/' . trim($mode, '/') . '/';
 245          $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
 246  
 247          if (!$preview) {
 248              $preview = $this->create_file_preview($file, $mode);
 249              if (!$preview) {
 250                  return false;
 251              }
 252          }
 253  
 254          return $preview;
 255      }
 256  
 257      /**
 258       * Return an available file name.
 259       *
 260       * This will return the next available file name in the area, adding/incrementing a suffix
 261       * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
 262       *
 263       * If the file name passed is available without modification, it is returned as is.
 264       *
 265       * @param int $contextid context ID.
 266       * @param string $component component.
 267       * @param string $filearea file area.
 268       * @param int $itemid area item ID.
 269       * @param string $filepath the file path.
 270       * @param string $filename the file name.
 271       * @return string available file name.
 272       * @throws coding_exception if the file name is invalid.
 273       * @since Moodle 2.5
 274       */
 275      public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
 276          global $DB;
 277  
 278          // Do not accept '.' or an empty file name (zero is acceptable).
 279          if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
 280              throw new coding_exception('Invalid file name passed', $filename);
 281          }
 282  
 283          // The file does not exist, we return the same file name.
 284          if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
 285              return $filename;
 286          }
 287  
 288          // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
 289          $pathinfo = pathinfo($filename);
 290          $basename = $pathinfo['filename'];
 291          $matches = array();
 292          if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
 293              $basename = $matches[1];
 294          }
 295  
 296          $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
 297          if (isset($pathinfo['extension'])) {
 298              $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
 299          }
 300  
 301          $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
 302          $filenamelen = $DB->sql_length('f.filename');
 303          $sql = "SELECT filename
 304                  FROM {files} f
 305                  WHERE
 306                      f.contextid = :contextid AND
 307                      f.component = :component AND
 308                      f.filearea = :filearea AND
 309                      f.itemid = :itemid AND
 310                      f.filepath = :filepath AND
 311                      $filenamelikesql
 312                  ORDER BY
 313                      $filenamelen DESC,
 314                      f.filename DESC";
 315          $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
 316                  'filepath' => $filepath, 'filenamelike' => $filenamelike);
 317          $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
 318  
 319          // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
 320          // would both be returned, but only the one only containing digits should be used.
 321          $number = 1;
 322          foreach ($results as $result) {
 323              $resultbasename = pathinfo($result, PATHINFO_FILENAME);
 324              $matches = array();
 325              if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
 326                  $number = $matches[2] + 1;
 327                  break;
 328              }
 329          }
 330  
 331          // Constructing the new filename.
 332          $newfilename = $basename . ' (' . $number . ')';
 333          if (isset($pathinfo['extension'])) {
 334              $newfilename .= '.' . $pathinfo['extension'];
 335          }
 336  
 337          return $newfilename;
 338      }
 339  
 340      /**
 341       * Return an available directory name.
 342       *
 343       * This will return the next available directory name in the area, adding/incrementing a suffix
 344       * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
 345       *
 346       * If the file path passed is available without modification, it is returned as is.
 347       *
 348       * @param int $contextid context ID.
 349       * @param string $component component.
 350       * @param string $filearea file area.
 351       * @param int $itemid area item ID.
 352       * @param string $suggestedpath the suggested file path.
 353       * @return string available file path
 354       * @since Moodle 2.5
 355       */
 356      public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
 357          global $DB;
 358  
 359          // Ensure suggestedpath has trailing '/'
 360          $suggestedpath = rtrim($suggestedpath, '/'). '/';
 361  
 362          // The directory does not exist, we return the same file path.
 363          if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
 364              return $suggestedpath;
 365          }
 366  
 367          // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
 368          if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
 369              $suggestedpath = $matches[1]. '/';
 370          }
 371  
 372          $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
 373  
 374          $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
 375          $filepathlen = $DB->sql_length('f.filepath');
 376          $sql = "SELECT filepath
 377                  FROM {files} f
 378                  WHERE
 379                      f.contextid = :contextid AND
 380                      f.component = :component AND
 381                      f.filearea = :filearea AND
 382                      f.itemid = :itemid AND
 383                      f.filename = :filename AND
 384                      $filepathlikesql
 385                  ORDER BY
 386                      $filepathlen DESC,
 387                      f.filepath DESC";
 388          $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
 389                  'filename' => '.', 'filepathlike' => $filepathlike);
 390          $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
 391  
 392          // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
 393          // would both be returned, but only the one only containing digits should be used.
 394          $number = 1;
 395          foreach ($results as $result) {
 396              if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
 397                  $number = (int)($matches[1]) + 1;
 398                  break;
 399              }
 400          }
 401  
 402          return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
 403      }
 404  
 405      /**
 406       * Generates a preview image for the stored file
 407       *
 408       * @param stored_file $file the file we want to preview
 409       * @param string $mode preview mode, eg. 'thumb'
 410       * @return stored_file|bool the newly created preview file or false
 411       */
 412      protected function create_file_preview(stored_file $file, $mode) {
 413  
 414          $mimetype = $file->get_mimetype();
 415  
 416          if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
 417              // make a preview of the image
 418              $data = $this->create_imagefile_preview($file, $mode);
 419          } else if ($mimetype === 'image/svg+xml') {
 420              // If we have an SVG image, then return the original (scalable) file.
 421              return $file;
 422          } else {
 423              // unable to create the preview of this mimetype yet
 424              return false;
 425          }
 426  
 427          if (empty($data)) {
 428              return false;
 429          }
 430  
 431          $context = context_system::instance();
 432          $record = array(
 433              'contextid' => $context->id,
 434              'component' => 'core',
 435              'filearea'  => 'preview',
 436              'itemid'    => 0,
 437              'filepath'  => '/' . trim($mode, '/') . '/',
 438              'filename'  => $file->get_contenthash(),
 439          );
 440  
 441          $imageinfo = getimagesizefromstring($data);
 442          if ($imageinfo) {
 443              $record['mimetype'] = $imageinfo['mime'];
 444          }
 445  
 446          return $this->create_file_from_string($record, $data);
 447      }
 448  
 449      /**
 450       * Generates a preview for the stored image file
 451       *
 452       * @param stored_file $file the image we want to preview
 453       * @param string $mode preview mode, eg. 'thumb'
 454       * @return string|bool false if a problem occurs, the thumbnail image data otherwise
 455       */
 456      protected function create_imagefile_preview(stored_file $file, $mode) {
 457          global $CFG;
 458          require_once($CFG->libdir.'/gdlib.php');
 459  
 460          if ($mode === 'tinyicon') {
 461              $data = $file->generate_image_thumbnail(24, 24);
 462  
 463          } else if ($mode === 'thumb') {
 464              $data = $file->generate_image_thumbnail(90, 90);
 465  
 466          } else if ($mode === 'bigthumb') {
 467              $data = $file->generate_image_thumbnail(250, 250);
 468  
 469          } else {
 470              throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
 471          }
 472  
 473          return $data;
 474      }
 475  
 476      /**
 477       * Fetch file using local file id.
 478       *
 479       * Please do not rely on file ids, it is usually easier to use
 480       * pathname hashes instead.
 481       *
 482       * @param int $fileid file ID
 483       * @return stored_file|bool stored_file instance if exists, false if not
 484       */
 485      public function get_file_by_id($fileid) {
 486          global $DB;
 487  
 488          $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 489                    FROM {files} f
 490               LEFT JOIN {files_reference} r
 491                         ON f.referencefileid = r.id
 492                   WHERE f.id = ?";
 493          if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
 494              return $this->get_file_instance($filerecord);
 495          } else {
 496              return false;
 497          }
 498      }
 499  
 500      /**
 501       * Fetch file using local file full pathname hash
 502       *
 503       * @param string $pathnamehash path name hash
 504       * @return stored_file|bool stored_file instance if exists, false if not
 505       */
 506      public function get_file_by_hash($pathnamehash) {
 507          global $DB;
 508  
 509          $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 510                    FROM {files} f
 511               LEFT JOIN {files_reference} r
 512                         ON f.referencefileid = r.id
 513                   WHERE f.pathnamehash = ?";
 514          if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
 515              return $this->get_file_instance($filerecord);
 516          } else {
 517              return false;
 518          }
 519      }
 520  
 521      /**
 522       * Fetch locally stored file.
 523       *
 524       * @param int $contextid context ID
 525       * @param string $component component
 526       * @param string $filearea file area
 527       * @param int $itemid item ID
 528       * @param string $filepath file path
 529       * @param string $filename file name
 530       * @return stored_file|bool stored_file instance if exists, false if not
 531       */
 532      public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
 533          $filepath = clean_param($filepath, PARAM_PATH);
 534          $filename = clean_param($filename, PARAM_FILE);
 535  
 536          if ($filename === '') {
 537              $filename = '.';
 538          }
 539  
 540          $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
 541          return $this->get_file_by_hash($pathnamehash);
 542      }
 543  
 544      /**
 545       * Are there any files (or directories)
 546       *
 547       * @param int $contextid context ID
 548       * @param string $component component
 549       * @param string $filearea file area
 550       * @param bool|int $itemid item id or false if all items
 551       * @param bool $ignoredirs whether or not ignore directories
 552       * @return bool empty
 553       */
 554      public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
 555          global $DB;
 556  
 557          $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
 558          $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
 559  
 560          if ($itemid !== false) {
 561              $params['itemid'] = $itemid;
 562              $where .= " AND itemid = :itemid";
 563          }
 564  
 565          if ($ignoredirs) {
 566              $sql = "SELECT 'x'
 567                        FROM {files}
 568                       WHERE $where AND filename <> '.'";
 569          } else {
 570              $sql = "SELECT 'x'
 571                        FROM {files}
 572                       WHERE $where AND (filename <> '.' OR filepath <> '/')";
 573          }
 574  
 575          return !$DB->record_exists_sql($sql, $params);
 576      }
 577  
 578      /**
 579       * Returns all files belonging to given repository
 580       *
 581       * @param int $repositoryid
 582       * @param string $sort A fragment of SQL to use for sorting
 583       */
 584      public function get_external_files($repositoryid, $sort = '') {
 585          global $DB;
 586          $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 587                    FROM {files} f
 588               LEFT JOIN {files_reference} r
 589                         ON f.referencefileid = r.id
 590                   WHERE r.repositoryid = ?";
 591          if (!empty($sort)) {
 592              $sql .= " ORDER BY {$sort}";
 593          }
 594  
 595          $result = array();
 596          $filerecords = $DB->get_records_sql($sql, array($repositoryid));
 597          foreach ($filerecords as $filerecord) {
 598              $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
 599          }
 600          return $result;
 601      }
 602  
 603      /**
 604       * Returns all area files (optionally limited by itemid)
 605       *
 606       * @param int $contextid context ID
 607       * @param string $component component
 608       * @param mixed $filearea file area/s, you cannot specify multiple fileareas as well as an itemid
 609       * @param int|int[]|false $itemid item ID(s) or all files if not specified
 610       * @param string $sort A fragment of SQL to use for sorting
 611       * @param bool $includedirs whether or not include directories
 612       * @param int $updatedsince return files updated since this time
 613       * @param int $limitfrom return a subset of records, starting at this point (optional).
 614       * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
 615       * @return stored_file[] array of stored_files indexed by pathanmehash
 616       */
 617      public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename",
 618              $includedirs = true, $updatedsince = 0, $limitfrom = 0, $limitnum = 0) {
 619          global $DB;
 620  
 621          list($areasql, $conditions) = $DB->get_in_or_equal($filearea, SQL_PARAMS_NAMED);
 622          $conditions['contextid'] = $contextid;
 623          $conditions['component'] = $component;
 624  
 625          if ($itemid !== false && is_array($filearea)) {
 626              throw new coding_exception('You cannot specify multiple fileareas as well as an itemid.');
 627          } else if ($itemid !== false) {
 628              $itemids = is_array($itemid) ? $itemid : [$itemid];
 629              list($itemidinorequalsql, $itemidconditions) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
 630              $itemidsql = " AND f.itemid {$itemidinorequalsql}";
 631              $conditions = array_merge($conditions, $itemidconditions);
 632          } else {
 633              $itemidsql = '';
 634          }
 635  
 636          $updatedsincesql = '';
 637          if (!empty($updatedsince)) {
 638              $conditions['time'] = $updatedsince;
 639              $updatedsincesql = 'AND f.timemodified > :time';
 640          }
 641  
 642          $includedirssql = '';
 643          if (!$includedirs) {
 644              $includedirssql = 'AND f.filename != :dot';
 645              $conditions['dot'] = '.';
 646          }
 647  
 648          if ($limitfrom && !$limitnum) {
 649              throw new coding_exception('If specifying $limitfrom you must also specify $limitnum');
 650          }
 651  
 652          $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 653                    FROM {files} f
 654               LEFT JOIN {files_reference} r
 655                         ON f.referencefileid = r.id
 656                   WHERE f.contextid = :contextid
 657                         AND f.component = :component
 658                         AND f.filearea $areasql
 659                         $includedirssql
 660                         $updatedsincesql
 661                         $itemidsql";
 662          if (!empty($sort)) {
 663              $sql .= " ORDER BY {$sort}";
 664          }
 665  
 666          $result = array();
 667          $filerecords = $DB->get_records_sql($sql, $conditions, $limitfrom, $limitnum);
 668          foreach ($filerecords as $filerecord) {
 669              $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
 670          }
 671          return $result;
 672      }
 673  
 674      /**
 675       * Returns the file area item ids and their updatetime for a user's draft uploads, sorted by updatetime DESC.
 676       *
 677       * @param int $userid user id
 678       * @param int $updatedsince only return draft areas updated since this time
 679       * @param int $lastnum only return the last specified numbers
 680       * @return array
 681       */
 682      public function get_user_draft_items(int $userid, int $updatedsince = 0, int $lastnum = 0): array {
 683          global $DB;
 684  
 685          $params = [
 686              'component' => 'user',
 687              'filearea' => 'draft',
 688              'contextid' => context_user::instance($userid)->id,
 689          ];
 690  
 691          $updatedsincesql = '';
 692          if ($updatedsince) {
 693              $updatedsincesql = 'AND f.timemodified > :time';
 694              $params['time'] = $updatedsince;
 695          }
 696          $sql = "SELECT itemid,
 697                         MAX(f.timemodified) AS timemodified
 698                    FROM {files} f
 699                   WHERE component = :component
 700                         AND filearea = :filearea
 701                         AND contextid = :contextid
 702                         $updatedsincesql
 703                GROUP BY itemid
 704                ORDER BY MAX(f.timemodified) DESC";
 705  
 706          return $DB->get_records_sql($sql, $params, 0, $lastnum);
 707      }
 708  
 709      /**
 710       * Returns array based tree structure of area files
 711       *
 712       * @param int $contextid context ID
 713       * @param string $component component
 714       * @param string $filearea file area
 715       * @param int $itemid item ID
 716       * @return array each dir represented by dirname, subdirs, files and dirfile array elements
 717       */
 718      public function get_area_tree($contextid, $component, $filearea, $itemid) {
 719          $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
 720          $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
 721          // first create directory structure
 722          foreach ($files as $hash=>$dir) {
 723              if (!$dir->is_directory()) {
 724                  continue;
 725              }
 726              unset($files[$hash]);
 727              if ($dir->get_filepath() === '/') {
 728                  $result['dirfile'] = $dir;
 729                  continue;
 730              }
 731              $parts = explode('/', trim($dir->get_filepath(),'/'));
 732              $pointer =& $result;
 733              foreach ($parts as $part) {
 734                  if ($part === '') {
 735                      continue;
 736                  }
 737                  if (!isset($pointer['subdirs'][$part])) {
 738                      $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
 739                  }
 740                  $pointer =& $pointer['subdirs'][$part];
 741              }
 742              $pointer['dirfile'] = $dir;
 743              unset($pointer);
 744          }
 745          foreach ($files as $hash=>$file) {
 746              $parts = explode('/', trim($file->get_filepath(),'/'));
 747              $pointer =& $result;
 748              foreach ($parts as $part) {
 749                  if ($part === '') {
 750                      continue;
 751                  }
 752                  $pointer =& $pointer['subdirs'][$part];
 753              }
 754              $pointer['files'][$file->get_filename()] = $file;
 755              unset($pointer);
 756          }
 757          $result = $this->sort_area_tree($result);
 758          return $result;
 759      }
 760  
 761      /**
 762       * Sorts the result of {@link file_storage::get_area_tree()}.
 763       *
 764       * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
 765       * @return array of sorted results
 766       */
 767      protected function sort_area_tree($tree) {
 768          foreach ($tree as $key => &$value) {
 769              if ($key == 'subdirs') {
 770                  core_collator::ksort($value, core_collator::SORT_NATURAL);
 771                  foreach ($value as $subdirname => &$subtree) {
 772                      $subtree = $this->sort_area_tree($subtree);
 773                  }
 774              } else if ($key == 'files') {
 775                  core_collator::ksort($value, core_collator::SORT_NATURAL);
 776              }
 777          }
 778          return $tree;
 779      }
 780  
 781      /**
 782       * Returns all files and optionally directories
 783       *
 784       * @param int $contextid context ID
 785       * @param string $component component
 786       * @param string $filearea file area
 787       * @param int $itemid item ID
 788       * @param int $filepath directory path
 789       * @param bool $recursive include all subdirectories
 790       * @param bool $includedirs include files and directories
 791       * @param string $sort A fragment of SQL to use for sorting
 792       * @return array of stored_files indexed by pathanmehash
 793       */
 794      public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
 795          global $DB;
 796  
 797          if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
 798              return array();
 799          }
 800  
 801          $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
 802  
 803          if ($recursive) {
 804  
 805              $dirs = $includedirs ? "" : "AND filename <> '.'";
 806              $length = core_text::strlen($filepath);
 807  
 808              $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 809                        FROM {files} f
 810                   LEFT JOIN {files_reference} r
 811                             ON f.referencefileid = r.id
 812                       WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
 813                             AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
 814                             AND f.id <> :dirid
 815                             $dirs
 816                             $orderby";
 817              $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
 818  
 819              $files = array();
 820              $dirs  = array();
 821              $filerecords = $DB->get_records_sql($sql, $params);
 822              foreach ($filerecords as $filerecord) {
 823                  if ($filerecord->filename == '.') {
 824                      $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
 825                  } else {
 826                      $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
 827                  }
 828              }
 829              $result = array_merge($dirs, $files);
 830  
 831          } else {
 832              $result = array();
 833              $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
 834  
 835              $length = core_text::strlen($filepath);
 836  
 837              if ($includedirs) {
 838                  $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 839                            FROM {files} f
 840                       LEFT JOIN {files_reference} r
 841                                 ON f.referencefileid = r.id
 842                           WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
 843                                 AND f.itemid = :itemid AND f.filename = '.'
 844                                 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
 845                                 AND f.id <> :dirid
 846                                 $orderby";
 847                  $reqlevel = substr_count($filepath, '/') + 1;
 848                  $filerecords = $DB->get_records_sql($sql, $params);
 849                  foreach ($filerecords as $filerecord) {
 850                      if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
 851                          continue;
 852                      }
 853                      $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
 854                  }
 855              }
 856  
 857              $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
 858                        FROM {files} f
 859                   LEFT JOIN {files_reference} r
 860                             ON f.referencefileid = r.id
 861                       WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
 862                             AND f.filepath = :filepath AND f.filename <> '.'
 863                             $orderby";
 864  
 865              $filerecords = $DB->get_records_sql($sql, $params);
 866              foreach ($filerecords as $filerecord) {
 867                  $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
 868              }
 869          }
 870  
 871          return $result;
 872      }
 873  
 874      /**
 875       * Delete all area files (optionally limited by itemid).
 876       *
 877       * @param int $contextid context ID
 878       * @param string $component component
 879       * @param string $filearea file area or all areas in context if not specified
 880       * @param int $itemid item ID or all files if not specified
 881       * @return bool success
 882       */
 883      public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
 884          global $DB;
 885  
 886          $conditions = array('contextid'=>$contextid);
 887          if ($component !== false) {
 888              $conditions['component'] = $component;
 889          }
 890          if ($filearea !== false) {
 891              $conditions['filearea'] = $filearea;
 892          }
 893          if ($itemid !== false) {
 894              $conditions['itemid'] = $itemid;
 895          }
 896  
 897          $filerecords = $DB->get_records('files', $conditions);
 898          foreach ($filerecords as $filerecord) {
 899              $this->get_file_instance($filerecord)->delete();
 900          }
 901  
 902          return true; // BC only
 903      }
 904  
 905      /**
 906       * Delete all the files from certain areas where itemid is limited by an
 907       * arbitrary bit of SQL.
 908       *
 909       * @param int $contextid the id of the context the files belong to. Must be given.
 910       * @param string $component the owning component. Must be given.
 911       * @param string $filearea the file area name. Must be given.
 912       * @param string $itemidstest an SQL fragment that the itemid must match. Used
 913       *      in the query like WHERE itemid $itemidstest. Must used named parameters,
 914       *      and may not used named parameters called contextid, component or filearea.
 915       * @param array $params any query params used by $itemidstest.
 916       */
 917      public function delete_area_files_select($contextid, $component,
 918              $filearea, $itemidstest, array $params = null) {
 919          global $DB;
 920  
 921          $where = "contextid = :contextid
 922                  AND component = :component
 923                  AND filearea = :filearea
 924                  AND itemid $itemidstest";
 925          $params['contextid'] = $contextid;
 926          $params['component'] = $component;
 927          $params['filearea'] = $filearea;
 928  
 929          $filerecords = $DB->get_recordset_select('files', $where, $params);
 930          foreach ($filerecords as $filerecord) {
 931              $this->get_file_instance($filerecord)->delete();
 932          }
 933          $filerecords->close();
 934      }
 935  
 936      /**
 937       * Delete all files associated with the given component.
 938       *
 939       * @param string $component the component owning the file
 940       */
 941      public function delete_component_files($component) {
 942          global $DB;
 943  
 944          $filerecords = $DB->get_recordset('files', array('component' => $component));
 945          foreach ($filerecords as $filerecord) {
 946              $this->get_file_instance($filerecord)->delete();
 947          }
 948          $filerecords->close();
 949      }
 950  
 951      /**
 952       * Move all the files in a file area from one context to another.
 953       *
 954       * @param int $oldcontextid the context the files are being moved from.
 955       * @param int $newcontextid the context the files are being moved to.
 956       * @param string $component the plugin that these files belong to.
 957       * @param string $filearea the name of the file area.
 958       * @param int $itemid file item ID
 959       * @return int the number of files moved, for information.
 960       */
 961      public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
 962          // Note, this code is based on some code that Petr wrote in
 963          // forum_move_attachments in mod/forum/lib.php. I moved it here because
 964          // I needed it in the question code too.
 965          $count = 0;
 966  
 967          $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
 968          foreach ($oldfiles as $oldfile) {
 969              $filerecord = new stdClass();
 970              $filerecord->contextid = $newcontextid;
 971              $this->create_file_from_storedfile($filerecord, $oldfile);
 972              $count += 1;
 973          }
 974  
 975          if ($count) {
 976              $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
 977          }
 978  
 979          return $count;
 980      }
 981  
 982      /**
 983       * Recursively creates directory.
 984       *
 985       * @param int $contextid context ID
 986       * @param string $component component
 987       * @param string $filearea file area
 988       * @param int $itemid item ID
 989       * @param string $filepath file path
 990       * @param int $userid the user ID
 991       * @return bool success
 992       */
 993      public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
 994          global $DB;
 995  
 996          // validate all parameters, we do not want any rubbish stored in database, right?
 997          if (!is_number($contextid) or $contextid < 1) {
 998              throw new file_exception('storedfileproblem', 'Invalid contextid');
 999          }
1000  
1001          $component = clean_param($component, PARAM_COMPONENT);
1002          if (empty($component)) {
1003              throw new file_exception('storedfileproblem', 'Invalid component');
1004          }
1005  
1006          $filearea = clean_param($filearea, PARAM_AREA);
1007          if (empty($filearea)) {
1008              throw new file_exception('storedfileproblem', 'Invalid filearea');
1009          }
1010  
1011          if (!is_number($itemid) or $itemid < 0) {
1012              throw new file_exception('storedfileproblem', 'Invalid itemid');
1013          }
1014  
1015          $filepath = clean_param($filepath, PARAM_PATH);
1016          if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1017              // path must start and end with '/'
1018              throw new file_exception('storedfileproblem', 'Invalid file path');
1019          }
1020  
1021          $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1022  
1023          if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1024              return $dir_info;
1025          }
1026  
1027          static $contenthash = null;
1028          if (!$contenthash) {
1029              $this->add_string_to_pool('');
1030              $contenthash = self::hash_from_string('');
1031          }
1032  
1033          $now = time();
1034  
1035          $dir_record = new stdClass();
1036          $dir_record->contextid = $contextid;
1037          $dir_record->component = $component;
1038          $dir_record->filearea  = $filearea;
1039          $dir_record->itemid    = $itemid;
1040          $dir_record->filepath  = $filepath;
1041          $dir_record->filename  = '.';
1042          $dir_record->contenthash  = $contenthash;
1043          $dir_record->filesize  = 0;
1044  
1045          $dir_record->timecreated  = $now;
1046          $dir_record->timemodified = $now;
1047          $dir_record->mimetype     = null;
1048          $dir_record->userid       = $userid;
1049  
1050          $dir_record->pathnamehash = $pathnamehash;
1051  
1052          $DB->insert_record('files', $dir_record);
1053          $dir_info = $this->get_file_by_hash($pathnamehash);
1054  
1055          if ($filepath !== '/') {
1056              //recurse to parent dirs
1057              $filepath = trim($filepath, '/');
1058              $filepath = explode('/', $filepath);
1059              array_pop($filepath);
1060              $filepath = implode('/', $filepath);
1061              $filepath = ($filepath === '') ? '/' : "/$filepath/";
1062              $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1063          }
1064  
1065          return $dir_info;
1066      }
1067  
1068      /**
1069       * Add new file record to database and handle callbacks.
1070       *
1071       * @param stdClass $newrecord
1072       */
1073      protected function create_file($newrecord) {
1074          global $DB;
1075          $newrecord->id = $DB->insert_record('files', $newrecord);
1076  
1077          if ($newrecord->filename !== '.') {
1078              // Callback for file created.
1079              if ($pluginsfunction = get_plugins_with_function('after_file_created')) {
1080                  foreach ($pluginsfunction as $plugintype => $plugins) {
1081                      foreach ($plugins as $pluginfunction) {
1082                          $pluginfunction($newrecord);
1083                      }
1084                  }
1085              }
1086          }
1087      }
1088  
1089      /**
1090       * Add new local file based on existing local file.
1091       *
1092       * @param stdClass|array $filerecord object or array describing changes
1093       * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1094       * @return stored_file instance of newly created file
1095       */
1096      public function create_file_from_storedfile($filerecord, $fileorid) {
1097          global $DB;
1098  
1099          if ($fileorid instanceof stored_file) {
1100              $fid = $fileorid->get_id();
1101          } else {
1102              $fid = $fileorid;
1103          }
1104  
1105          $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1106  
1107          unset($filerecord['id']);
1108          unset($filerecord['filesize']);
1109          unset($filerecord['contenthash']);
1110          unset($filerecord['pathnamehash']);
1111  
1112          $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1113                    FROM {files} f
1114               LEFT JOIN {files_reference} r
1115                         ON f.referencefileid = r.id
1116                   WHERE f.id = ?";
1117  
1118          if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1119              throw new file_exception('storedfileproblem', 'File does not exist');
1120          }
1121  
1122          unset($newrecord->id);
1123  
1124          foreach ($filerecord as $key => $value) {
1125              // validate all parameters, we do not want any rubbish stored in database, right?
1126              if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1127                  throw new file_exception('storedfileproblem', 'Invalid contextid');
1128              }
1129  
1130              if ($key == 'component') {
1131                  $value = clean_param($value, PARAM_COMPONENT);
1132                  if (empty($value)) {
1133                      throw new file_exception('storedfileproblem', 'Invalid component');
1134                  }
1135              }
1136  
1137              if ($key == 'filearea') {
1138                  $value = clean_param($value, PARAM_AREA);
1139                  if (empty($value)) {
1140                      throw new file_exception('storedfileproblem', 'Invalid filearea');
1141                  }
1142              }
1143  
1144              if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1145                  throw new file_exception('storedfileproblem', 'Invalid itemid');
1146              }
1147  
1148  
1149              if ($key == 'filepath') {
1150                  $value = clean_param($value, PARAM_PATH);
1151                  if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1152                      // path must start and end with '/'
1153                      throw new file_exception('storedfileproblem', 'Invalid file path');
1154                  }
1155              }
1156  
1157              if ($key == 'filename') {
1158                  $value = clean_param($value, PARAM_FILE);
1159                  if ($value === '') {
1160                      // path must start and end with '/'
1161                      throw new file_exception('storedfileproblem', 'Invalid file name');
1162                  }
1163              }
1164  
1165              if ($key === 'timecreated' or $key === 'timemodified') {
1166                  if (!is_number($value)) {
1167                      throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1168                  }
1169                  if ($value < 0) {
1170                      //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1171                      $value = 0;
1172                  }
1173              }
1174  
1175              if ($key == 'referencefileid' or $key == 'referencelastsync') {
1176                  $value = clean_param($value, PARAM_INT);
1177              }
1178  
1179              $newrecord->$key = $value;
1180          }
1181  
1182          $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1183  
1184          if ($newrecord->filename === '.') {
1185              // special case - only this function supports directories ;-)
1186              $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1187              // update the existing directory with the new data
1188              $newrecord->id = $directory->get_id();
1189              $DB->update_record('files', $newrecord);
1190              return $this->get_file_instance($newrecord);
1191          }
1192  
1193          // note: referencefileid is copied from the original file so that
1194          // creating a new file from an existing alias creates new alias implicitly.
1195          // here we just check the database consistency.
1196          if (!empty($newrecord->repositoryid)) {
1197              // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files
1198              // where saved from a draft area.
1199              $newrecord->referencefileid = $this->get_or_create_referencefileid($newrecord->repositoryid, $newrecord->reference);
1200          }
1201  
1202          try {
1203              $this->create_file($newrecord);
1204          } catch (dml_exception $e) {
1205              throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1206                                                       $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1207          }
1208  
1209  
1210          $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1211  
1212          return $this->get_file_instance($newrecord);
1213      }
1214  
1215      /**
1216       * Add new local file.
1217       *
1218       * @param stdClass|array $filerecord object or array describing file
1219       * @param string $url the URL to the file
1220       * @param array $options {@link download_file_content()} options
1221       * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1222       * @return stored_file
1223       */
1224      public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1225  
1226          $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1227          $filerecord = (object)$filerecord; // We support arrays too.
1228  
1229          $headers        = isset($options['headers'])        ? $options['headers'] : null;
1230          $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1231          $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1232          $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1233          $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1234          $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1235          $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1236  
1237          if (!isset($filerecord->filename)) {
1238              $parts = explode('/', $url);
1239              $filename = array_pop($parts);
1240              $filerecord->filename = clean_param($filename, PARAM_FILE);
1241          }
1242          $source = !empty($filerecord->source) ? $filerecord->source : $url;
1243          $filerecord->source = clean_param($source, PARAM_URL);
1244  
1245          if ($usetempfile) {
1246              check_dir_exists($this->tempdir);
1247              $tmpfile = tempnam($this->tempdir, 'newfromurl');
1248              $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1249              if ($content === false) {
1250                  throw new file_exception('storedfileproblem', 'Cannot fetch file from URL');
1251              }
1252              try {
1253                  $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1254                  @unlink($tmpfile);
1255                  return $newfile;
1256              } catch (Exception $e) {
1257                  @unlink($tmpfile);
1258                  throw $e;
1259              }
1260  
1261          } else {
1262              $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1263              if ($content === false) {
1264                  throw new file_exception('storedfileproblem', 'Cannot fetch file from URL');
1265              }
1266              return $this->create_file_from_string($filerecord, $content);
1267          }
1268      }
1269  
1270      /**
1271       * Add new local file.
1272       *
1273       * @param stdClass|array $filerecord object or array describing file
1274       * @param string $pathname path to file or content of file
1275       * @return stored_file
1276       */
1277      public function create_file_from_pathname($filerecord, $pathname) {
1278          global $DB;
1279  
1280          $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1281          $filerecord = (object)$filerecord; // We support arrays too.
1282  
1283          // validate all parameters, we do not want any rubbish stored in database, right?
1284          if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1285              throw new file_exception('storedfileproblem', 'Invalid contextid');
1286          }
1287  
1288          $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1289          if (empty($filerecord->component)) {
1290              throw new file_exception('storedfileproblem', 'Invalid component');
1291          }
1292  
1293          $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1294          if (empty($filerecord->filearea)) {
1295              throw new file_exception('storedfileproblem', 'Invalid filearea');
1296          }
1297  
1298          if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1299              throw new file_exception('storedfileproblem', 'Invalid itemid');
1300          }
1301  
1302          if (!empty($filerecord->sortorder)) {
1303              if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1304                  $filerecord->sortorder = 0;
1305              }
1306          } else {
1307              $filerecord->sortorder = 0;
1308          }
1309  
1310          $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1311          if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1312              // path must start and end with '/'
1313              throw new file_exception('storedfileproblem', 'Invalid file path');
1314          }
1315  
1316          $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1317          if ($filerecord->filename === '') {
1318              // filename must not be empty
1319              throw new file_exception('storedfileproblem', 'Invalid file name');
1320          }
1321  
1322          $now = time();
1323          if (isset($filerecord->timecreated)) {
1324              if (!is_number($filerecord->timecreated)) {
1325                  throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1326              }
1327              if ($filerecord->timecreated < 0) {
1328                  //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1329                  $filerecord->timecreated = 0;
1330              }
1331          } else {
1332              $filerecord->timecreated = $now;
1333          }
1334  
1335          if (isset($filerecord->timemodified)) {
1336              if (!is_number($filerecord->timemodified)) {
1337                  throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1338              }
1339              if ($filerecord->timemodified < 0) {
1340                  //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1341                  $filerecord->timemodified = 0;
1342              }
1343          } else {
1344              $filerecord->timemodified = $now;
1345          }
1346  
1347          $newrecord = new stdClass();
1348  
1349          $newrecord->contextid = $filerecord->contextid;
1350          $newrecord->component = $filerecord->component;
1351          $newrecord->filearea  = $filerecord->filearea;
1352          $newrecord->itemid    = $filerecord->itemid;
1353          $newrecord->filepath  = $filerecord->filepath;
1354          $newrecord->filename  = $filerecord->filename;
1355  
1356          $newrecord->timecreated  = $filerecord->timecreated;
1357          $newrecord->timemodified = $filerecord->timemodified;
1358          $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1359          $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1360          $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1361          $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1362          $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1363          $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1364          $newrecord->sortorder    = $filerecord->sortorder;
1365  
1366          list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname, null, $newrecord);
1367  
1368          $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1369  
1370          try {
1371              $this->create_file($newrecord);
1372          } catch (dml_exception $e) {
1373              if ($newfile) {
1374                  $this->filesystem->remove_file($newrecord->contenthash);
1375              }
1376              throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1377                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1378          }
1379  
1380          $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1381  
1382          return $this->get_file_instance($newrecord);
1383      }
1384  
1385      /**
1386       * Add new local file.
1387       *
1388       * @param stdClass|array $filerecord object or array describing file
1389       * @param string $content content of file
1390       * @return stored_file
1391       */
1392      public function create_file_from_string($filerecord, $content) {
1393          global $DB;
1394  
1395          $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1396          $filerecord = (object)$filerecord; // We support arrays too.
1397  
1398          // validate all parameters, we do not want any rubbish stored in database, right?
1399          if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1400              throw new file_exception('storedfileproblem', 'Invalid contextid');
1401          }
1402  
1403          $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1404          if (empty($filerecord->component)) {
1405              throw new file_exception('storedfileproblem', 'Invalid component');
1406          }
1407  
1408          $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1409          if (empty($filerecord->filearea)) {
1410              throw new file_exception('storedfileproblem', 'Invalid filearea');
1411          }
1412  
1413          if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1414              throw new file_exception('storedfileproblem', 'Invalid itemid');
1415          }
1416  
1417          if (!empty($filerecord->sortorder)) {
1418              if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1419                  $filerecord->sortorder = 0;
1420              }
1421          } else {
1422              $filerecord->sortorder = 0;
1423          }
1424  
1425          $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1426          if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1427              // path must start and end with '/'
1428              throw new file_exception('storedfileproblem', 'Invalid file path');
1429          }
1430  
1431          $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1432          if ($filerecord->filename === '') {
1433              // path must start and end with '/'
1434              throw new file_exception('storedfileproblem', 'Invalid file name');
1435          }
1436  
1437          $now = time();
1438          if (isset($filerecord->timecreated)) {
1439              if (!is_number($filerecord->timecreated)) {
1440                  throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1441              }
1442              if ($filerecord->timecreated < 0) {
1443                  //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1444                  $filerecord->timecreated = 0;
1445              }
1446          } else {
1447              $filerecord->timecreated = $now;
1448          }
1449  
1450          if (isset($filerecord->timemodified)) {
1451              if (!is_number($filerecord->timemodified)) {
1452                  throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1453              }
1454              if ($filerecord->timemodified < 0) {
1455                  //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1456                  $filerecord->timemodified = 0;
1457              }
1458          } else {
1459              $filerecord->timemodified = $now;
1460          }
1461  
1462          $newrecord = new stdClass();
1463  
1464          $newrecord->contextid = $filerecord->contextid;
1465          $newrecord->component = $filerecord->component;
1466          $newrecord->filearea  = $filerecord->filearea;
1467          $newrecord->itemid    = $filerecord->itemid;
1468          $newrecord->filepath  = $filerecord->filepath;
1469          $newrecord->filename  = $filerecord->filename;
1470  
1471          $newrecord->timecreated  = $filerecord->timecreated;
1472          $newrecord->timemodified = $filerecord->timemodified;
1473          $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1474          $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1475          $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1476          $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1477          $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1478          $newrecord->sortorder    = $filerecord->sortorder;
1479  
1480          list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content, $newrecord);
1481          if (empty($filerecord->mimetype)) {
1482              $newrecord->mimetype = $this->filesystem->mimetype_from_hash($newrecord->contenthash, $newrecord->filename);
1483          } else {
1484              $newrecord->mimetype = $filerecord->mimetype;
1485          }
1486  
1487          $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1488  
1489          try {
1490              $this->create_file($newrecord);
1491          } catch (dml_exception $e) {
1492              if ($newfile) {
1493                  $this->filesystem->remove_file($newrecord->contenthash);
1494              }
1495              throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1496                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1497          }
1498  
1499          $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1500  
1501          return $this->get_file_instance($newrecord);
1502      }
1503  
1504      /**
1505       * Synchronise stored file from file.
1506       *
1507       * @param stored_file $file Stored file to synchronise.
1508       * @param string $path Path to the file to synchronise from.
1509       * @param stdClass $filerecord The file record from the database.
1510       */
1511      public function synchronise_stored_file_from_file(stored_file $file, $path, $filerecord) {
1512          list($contenthash, $filesize) = $this->add_file_to_pool($path, null, $filerecord);
1513          $file->set_synchronized($contenthash, $filesize);
1514      }
1515  
1516      /**
1517       * Synchronise stored file from string.
1518       *
1519       * @param stored_file $file Stored file to synchronise.
1520       * @param string $content File content.
1521       * @param stdClass $filerecord The file record from the database.
1522       */
1523      public function synchronise_stored_file_from_string(stored_file $file, $content, $filerecord) {
1524          list($contenthash, $filesize) = $this->add_string_to_pool($content, $filerecord);
1525          $file->set_synchronized($contenthash, $filesize);
1526      }
1527  
1528      /**
1529       * Create a new alias/shortcut file from file reference information
1530       *
1531       * @param stdClass|array $filerecord object or array describing the new file
1532       * @param int $repositoryid the id of the repository that provides the original file
1533       * @param string $reference the information required by the repository to locate the original file
1534       * @param array $options options for creating the new file
1535       * @return stored_file
1536       */
1537      public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1538          global $DB;
1539  
1540          $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1541          $filerecord = (object)$filerecord; // We support arrays too.
1542  
1543          // validate all parameters, we do not want any rubbish stored in database, right?
1544          if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1545              throw new file_exception('storedfileproblem', 'Invalid contextid');
1546          }
1547  
1548          $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1549          if (empty($filerecord->component)) {
1550              throw new file_exception('storedfileproblem', 'Invalid component');
1551          }
1552  
1553          $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1554          if (empty($filerecord->filearea)) {
1555              throw new file_exception('storedfileproblem', 'Invalid filearea');
1556          }
1557  
1558          if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1559              throw new file_exception('storedfileproblem', 'Invalid itemid');
1560          }
1561  
1562          if (!empty($filerecord->sortorder)) {
1563              if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1564                  $filerecord->sortorder = 0;
1565              }
1566          } else {
1567              $filerecord->sortorder = 0;
1568          }
1569  
1570          $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1571          $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1572          $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1573          $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1574          $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1575          $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1576          $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1577          if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1578              // Path must start and end with '/'.
1579              throw new file_exception('storedfileproblem', 'Invalid file path');
1580          }
1581  
1582          $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1583          if ($filerecord->filename === '') {
1584              // Path must start and end with '/'.
1585              throw new file_exception('storedfileproblem', 'Invalid file name');
1586          }
1587  
1588          $now = time();
1589          if (isset($filerecord->timecreated)) {
1590              if (!is_number($filerecord->timecreated)) {
1591                  throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1592              }
1593              if ($filerecord->timecreated < 0) {
1594                  // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1595                  $filerecord->timecreated = 0;
1596              }
1597          } else {
1598              $filerecord->timecreated = $now;
1599          }
1600  
1601          if (isset($filerecord->timemodified)) {
1602              if (!is_number($filerecord->timemodified)) {
1603                  throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1604              }
1605              if ($filerecord->timemodified < 0) {
1606                  // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1607                  $filerecord->timemodified = 0;
1608              }
1609          } else {
1610              $filerecord->timemodified = $now;
1611          }
1612  
1613          $transaction = $DB->start_delegated_transaction();
1614  
1615          try {
1616              $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1617          } catch (Exception $e) {
1618              throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1619          }
1620  
1621          $existingfile = null;
1622          if (isset($filerecord->contenthash)) {
1623              $existingfile = $DB->get_record('files', array('contenthash' => $filerecord->contenthash), '*', IGNORE_MULTIPLE);
1624          }
1625          if (!empty($existingfile)) {
1626              // There is an existing file already available.
1627              if (empty($filerecord->filesize)) {
1628                  $filerecord->filesize = $existingfile->filesize;
1629              } else {
1630                  $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1631              }
1632          } else {
1633              // Attempt to get the result of last synchronisation for this reference.
1634              $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1635                      'id, contenthash, filesize', IGNORE_MULTIPLE);
1636              if ($lastcontent) {
1637                  $filerecord->contenthash = $lastcontent->contenthash;
1638                  $filerecord->filesize = $lastcontent->filesize;
1639              } else {
1640                  // External file doesn't have content in moodle.
1641                  // So we create an empty file for it.
1642                  list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null, $filerecord);
1643              }
1644          }
1645  
1646          $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1647  
1648          try {
1649              $filerecord->id = $DB->insert_record('files', $filerecord);
1650          } catch (dml_exception $e) {
1651              if (!empty($newfile)) {
1652                  $this->filesystem->remove_file($filerecord->contenthash);
1653              }
1654              throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1655                                                      $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1656          }
1657  
1658          $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1659  
1660          $transaction->allow_commit();
1661  
1662          // this will retrieve all reference information from DB as well
1663          return $this->get_file_by_id($filerecord->id);
1664      }
1665  
1666      /**
1667       * Creates new image file from existing.
1668       *
1669       * @param stdClass|array $filerecord object or array describing new file
1670       * @param int|stored_file $fid file id or stored file object
1671       * @param int $newwidth in pixels
1672       * @param int $newheight in pixels
1673       * @param bool $keepaspectratio whether or not keep aspect ratio
1674       * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1675       * @return stored_file
1676       */
1677      public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1678          if (!function_exists('imagecreatefromstring')) {
1679              //Most likely the GD php extension isn't installed
1680              //image conversion cannot succeed
1681              throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1682          }
1683  
1684          if ($fid instanceof stored_file) {
1685              $fid = $fid->get_id();
1686          }
1687  
1688          $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1689  
1690          if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1691              throw new file_exception('storedfileproblem', 'File does not exist');
1692          }
1693  
1694          if (!$imageinfo = $file->get_imageinfo()) {
1695              throw new file_exception('storedfileproblem', 'File is not an image');
1696          }
1697  
1698          if (!isset($filerecord['filename'])) {
1699              $filerecord['filename'] = $file->get_filename();
1700          }
1701  
1702          if (!isset($filerecord['mimetype'])) {
1703              $filerecord['mimetype'] = $imageinfo['mimetype'];
1704          }
1705  
1706          $width    = $imageinfo['width'];
1707          $height   = $imageinfo['height'];
1708  
1709          if ($keepaspectratio) {
1710              if (0 >= $newwidth and 0 >= $newheight) {
1711                  // no sizes specified
1712                  $newwidth  = $width;
1713                  $newheight = $height;
1714  
1715              } else if (0 < $newwidth and 0 < $newheight) {
1716                  $xheight = ($newwidth*($height/$width));
1717                  if ($xheight < $newheight) {
1718                      $newheight = (int)$xheight;
1719                  } else {
1720                      $newwidth = (int)($newheight*($width/$height));
1721                  }
1722  
1723              } else if (0 < $newwidth) {
1724                  $newheight = (int)($newwidth*($height/$width));
1725  
1726              } else { //0 < $newheight
1727                  $newwidth = (int)($newheight*($width/$height));
1728              }
1729  
1730          } else {
1731              if (0 >= $newwidth) {
1732                  $newwidth = $width;
1733              }
1734              if (0 >= $newheight) {
1735                  $newheight = $height;
1736              }
1737          }
1738  
1739          // The original image.
1740          $img = imagecreatefromstring($file->get_content());
1741  
1742          // A new true color image where we will copy our original image.
1743          $newimg = imagecreatetruecolor($newwidth, $newheight);
1744  
1745          // Determine if the file supports transparency.
1746          $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1747  
1748          // Maintain transparency.
1749          if ($hasalpha) {
1750              imagealphablending($newimg, true);
1751  
1752              // Get the current transparent index for the original image.
1753              $colour = imagecolortransparent($img);
1754              if ($colour == -1) {
1755                  // Set a transparent colour index if there's none.
1756                  $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1757                  // Save full alpha channel.
1758                  imagesavealpha($newimg, true);
1759              }
1760              imagecolortransparent($newimg, $colour);
1761              imagefill($newimg, 0, 0, $colour);
1762          }
1763  
1764          // Process the image to be output.
1765          if ($height != $newheight or $width != $newwidth) {
1766              // Resample if the dimensions differ from the original.
1767              if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1768                  // weird
1769                  throw new file_exception('storedfileproblem', 'Can not resize image');
1770              }
1771              imagedestroy($img);
1772              $img = $newimg;
1773  
1774          } else if ($hasalpha) {
1775              // Just copy to the new image with the alpha channel.
1776              if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1777                  // Weird.
1778                  throw new file_exception('storedfileproblem', 'Can not copy image');
1779              }
1780              imagedestroy($img);
1781              $img = $newimg;
1782  
1783          } else {
1784              // No particular processing needed for the original image.
1785              imagedestroy($newimg);
1786          }
1787  
1788          ob_start();
1789          switch ($filerecord['mimetype']) {
1790              case 'image/gif':
1791                  imagegif($img);
1792                  break;
1793  
1794              case 'image/jpeg':
1795                  if (is_null($quality)) {
1796                      imagejpeg($img);
1797                  } else {
1798                      imagejpeg($img, NULL, $quality);
1799                  }
1800                  break;
1801  
1802              case 'image/png':
1803                  $quality = (int)$quality;
1804  
1805                  // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1806                  // the latter of which can go to 100, we need to make sure that quality here is
1807                  // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1808                  $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1809                  imagepng($img, null, $quality, PNG_NO_FILTER);
1810                  break;
1811  
1812              default:
1813                  throw new file_exception('storedfileproblem', 'Unsupported mime type');
1814          }
1815  
1816          $content = ob_get_contents();
1817          ob_end_clean();
1818          imagedestroy($img);
1819  
1820          if (!$content) {
1821              throw new file_exception('storedfileproblem', 'Can not convert image');
1822          }
1823  
1824          return $this->create_file_from_string($filerecord, $content);
1825      }
1826  
1827      /**
1828       * Add file content to sha1 pool.
1829       *
1830       * @param string $pathname path to file
1831       * @param string|null $contenthash sha1 hash of content if known (performance only)
1832       * @param stdClass|null $newrecord New file record
1833       * @return array (contenthash, filesize, newfile)
1834       */
1835      public function add_file_to_pool($pathname, $contenthash = null, $newrecord = null) {
1836          $this->call_before_file_created_plugin_functions($newrecord, $pathname);
1837          return $this->filesystem->add_file_from_path($pathname, $contenthash);
1838      }
1839  
1840      /**
1841       * Add string content to sha1 pool.
1842       *
1843       * @param string $content file content - binary string
1844       * @return array (contenthash, filesize, newfile)
1845       */
1846      public function add_string_to_pool($content, $newrecord = null) {
1847          $this->call_before_file_created_plugin_functions($newrecord, null, $content);
1848          return $this->filesystem->add_file_from_string($content);
1849      }
1850  
1851      /**
1852       * before_file_created hook.
1853       *
1854       * @param stdClass|null $newrecord New file record.
1855       * @param string|null $pathname Path to file.
1856       * @param string|null $content File content.
1857       */
1858      protected function call_before_file_created_plugin_functions($newrecord, $pathname = null, $content = null) {
1859          $pluginsfunction = get_plugins_with_function('before_file_created');
1860          foreach ($pluginsfunction as $plugintype => $plugins) {
1861              foreach ($plugins as $pluginfunction) {
1862                  $pluginfunction($newrecord, ['pathname' => $pathname, 'content' => $content]);
1863              }
1864          }
1865      }
1866  
1867      /**
1868       * Serve file content using X-Sendfile header.
1869       * Please make sure that all headers are already sent and the all
1870       * access control checks passed.
1871       *
1872       * This alternate method to xsendfile() allows an alternate file system
1873       * to use the full file metadata and avoid extra lookups.
1874       *
1875       * @param stored_file $file The file to send
1876       * @return bool success
1877       */
1878      public function xsendfile_file(stored_file $file): bool {
1879          return $this->filesystem->xsendfile_file($file);
1880      }
1881  
1882      /**
1883       * Serve file content using X-Sendfile header.
1884       * Please make sure that all headers are already sent
1885       * and the all access control checks passed.
1886       *
1887       * @param string $contenthash sah1 hash of the file content to be served
1888       * @return bool success
1889       */
1890      public function xsendfile($contenthash) {
1891          return $this->filesystem->xsendfile($contenthash);
1892      }
1893  
1894      /**
1895       * Returns true if filesystem is configured to support xsendfile.
1896       *
1897       * @return bool
1898       */
1899      public function supports_xsendfile() {
1900          return $this->filesystem->supports_xsendfile();
1901      }
1902  
1903      /**
1904       * Content exists
1905       *
1906       * @param string $contenthash
1907       * @return bool
1908       * @deprecated since 3.3
1909       */
1910      public function content_exists($contenthash) {
1911          debugging('The content_exists function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1912  
1913          return false;
1914      }
1915  
1916      /**
1917       * Tries to recover missing content of file from trash.
1918       *
1919       * @param stored_file $file stored_file instance
1920       * @return bool success
1921       * @deprecated since 3.3
1922       */
1923      public function try_content_recovery($file) {
1924          debugging('The try_content_recovery function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1925  
1926          return false;
1927      }
1928  
1929      /**
1930       * When user referring to a moodle file, we build the reference field
1931       *
1932       * @param array $params
1933       * @return string
1934       */
1935      public static function pack_reference($params) {
1936          $params = (array)$params;
1937          $reference = array();
1938          $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1939          $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1940          $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1941          $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1942          $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1943          $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1944          return base64_encode(serialize($reference));
1945      }
1946  
1947      /**
1948       * Unpack reference field
1949       *
1950       * @param string $str
1951       * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1952       * @throws file_reference_exception if the $str does not have the expected format
1953       * @return array
1954       */
1955      public static function unpack_reference($str, $cleanparams = false) {
1956          $decoded = base64_decode($str, true);
1957          if ($decoded === false) {
1958              throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1959          }
1960          $params = unserialize_array($decoded);
1961          if ($params === false) {
1962              throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1963          }
1964          if (is_array($params) && $cleanparams) {
1965              $params = array(
1966                  'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1967                  'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1968                  'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1969                  'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1970                  'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1971                  'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1972              );
1973          }
1974          return $params;
1975      }
1976  
1977      /**
1978       * Search through the server files.
1979       *
1980       * The query parameter will be used in conjuction with the SQL directive
1981       * LIKE, so include '%' in it if you need to. This search will always ignore
1982       * user files and directories. Note that the search is case insensitive.
1983       *
1984       * This query can quickly become inefficient so use it sparignly.
1985       *
1986       * @param  string  $query The string used with SQL LIKE.
1987       * @param  integer $from  The offset to start the search at.
1988       * @param  integer $limit The maximum number of results.
1989       * @param  boolean $count When true this methods returns the number of results availabe,
1990       *                        disregarding the parameters $from and $limit.
1991       * @return int|array      Integer when count, otherwise array of stored_file objects.
1992       */
1993      public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
1994          global $DB;
1995          $params = array(
1996              'contextlevel' => CONTEXT_USER,
1997              'directory' => '.',
1998              'query' => $query
1999          );
2000  
2001          if ($count) {
2002              $select = 'COUNT(1)';
2003          } else {
2004              $select = self::instance_sql_fields('f', 'r');
2005          }
2006          $like = $DB->sql_like('f.filename', ':query', false);
2007  
2008          $sql = "SELECT $select
2009                    FROM {files} f
2010               LEFT JOIN {files_reference} r
2011                      ON f.referencefileid = r.id
2012                    JOIN {context} c
2013                      ON f.contextid = c.id
2014                   WHERE c.contextlevel <> :contextlevel
2015                     AND f.filename <> :directory
2016                     AND " . $like . "";
2017  
2018          if ($count) {
2019              return $DB->count_records_sql($sql, $params);
2020          }
2021  
2022          $sql .= " ORDER BY f.filename";
2023  
2024          $result = array();
2025          $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2026          foreach ($filerecords as $filerecord) {
2027              $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2028          }
2029          $filerecords->close();
2030  
2031          return $result;
2032      }
2033  
2034      /**
2035       * Returns all aliases that refer to some stored_file via the given reference
2036       *
2037       * All repositories that provide access to a stored_file are expected to use
2038       * {@link self::pack_reference()}. This method can't be used if the given reference
2039       * does not use this format or if you are looking for references to an external file
2040       * (for example it can't be used to search for all aliases that refer to a given
2041       * Dropbox or Box.net file).
2042       *
2043       * Aliases in user draft areas are excluded from the returned list.
2044       *
2045       * @param string $reference identification of the referenced file
2046       * @return array of stored_file indexed by its pathnamehash
2047       */
2048      public function search_references($reference) {
2049          global $DB;
2050  
2051          if (is_null($reference)) {
2052              throw new coding_exception('NULL is not a valid reference to an external file');
2053          }
2054  
2055          // Give {@link self::unpack_reference()} a chance to throw exception if the
2056          // reference is not in a valid format.
2057          self::unpack_reference($reference);
2058  
2059          $referencehash = sha1($reference);
2060  
2061          $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2062                    FROM {files} f
2063                    JOIN {files_reference} r ON f.referencefileid = r.id
2064                    JOIN {repository_instances} ri ON r.repositoryid = ri.id
2065                   WHERE r.referencehash = ?
2066                         AND (f.component <> ? OR f.filearea <> ?)";
2067  
2068          $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2069          $files = array();
2070          foreach ($rs as $filerecord) {
2071              $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2072          }
2073          $rs->close();
2074  
2075          return $files;
2076      }
2077  
2078      /**
2079       * Returns the number of aliases that refer to some stored_file via the given reference
2080       *
2081       * All repositories that provide access to a stored_file are expected to use
2082       * {@link self::pack_reference()}. This method can't be used if the given reference
2083       * does not use this format or if you are looking for references to an external file
2084       * (for example it can't be used to count aliases that refer to a given Dropbox or
2085       * Box.net file).
2086       *
2087       * Aliases in user draft areas are not counted.
2088       *
2089       * @param string $reference identification of the referenced file
2090       * @return int
2091       */
2092      public function search_references_count($reference) {
2093          global $DB;
2094  
2095          if (is_null($reference)) {
2096              throw new coding_exception('NULL is not a valid reference to an external file');
2097          }
2098  
2099          // Give {@link self::unpack_reference()} a chance to throw exception if the
2100          // reference is not in a valid format.
2101          self::unpack_reference($reference);
2102  
2103          $referencehash = sha1($reference);
2104  
2105          $sql = "SELECT COUNT(f.id)
2106                    FROM {files} f
2107                    JOIN {files_reference} r ON f.referencefileid = r.id
2108                    JOIN {repository_instances} ri ON r.repositoryid = ri.id
2109                   WHERE r.referencehash = ?
2110                         AND (f.component <> ? OR f.filearea <> ?)";
2111  
2112          return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2113      }
2114  
2115      /**
2116       * Returns all aliases that link to the given stored_file
2117       *
2118       * Aliases in user draft areas are excluded from the returned list.
2119       *
2120       * @param stored_file $storedfile
2121       * @return array of stored_file
2122       */
2123      public function get_references_by_storedfile(stored_file $storedfile) {
2124          global $DB;
2125  
2126          $params = array();
2127          $params['contextid'] = $storedfile->get_contextid();
2128          $params['component'] = $storedfile->get_component();
2129          $params['filearea']  = $storedfile->get_filearea();
2130          $params['itemid']    = $storedfile->get_itemid();
2131          $params['filename']  = $storedfile->get_filename();
2132          $params['filepath']  = $storedfile->get_filepath();
2133  
2134          return $this->search_references(self::pack_reference($params));
2135      }
2136  
2137      /**
2138       * Returns the number of aliases that link to the given stored_file
2139       *
2140       * Aliases in user draft areas are not counted.
2141       *
2142       * @param stored_file $storedfile
2143       * @return int
2144       */
2145      public function get_references_count_by_storedfile(stored_file $storedfile) {
2146          global $DB;
2147  
2148          $params = array();
2149          $params['contextid'] = $storedfile->get_contextid();
2150          $params['component'] = $storedfile->get_component();
2151          $params['filearea']  = $storedfile->get_filearea();
2152          $params['itemid']    = $storedfile->get_itemid();
2153          $params['filename']  = $storedfile->get_filename();
2154          $params['filepath']  = $storedfile->get_filepath();
2155  
2156          return $this->search_references_count(self::pack_reference($params));
2157      }
2158  
2159      /**
2160       * Updates all files that are referencing this file with the new contenthash
2161       * and filesize
2162       *
2163       * @param stored_file $storedfile
2164       */
2165      public function update_references_to_storedfile(stored_file $storedfile) {
2166          global $CFG, $DB;
2167          $params = array();
2168          $params['contextid'] = $storedfile->get_contextid();
2169          $params['component'] = $storedfile->get_component();
2170          $params['filearea']  = $storedfile->get_filearea();
2171          $params['itemid']    = $storedfile->get_itemid();
2172          $params['filename']  = $storedfile->get_filename();
2173          $params['filepath']  = $storedfile->get_filepath();
2174          $reference = self::pack_reference($params);
2175          $referencehash = sha1($reference);
2176  
2177          $sql = "SELECT repositoryid, id FROM {files_reference}
2178                   WHERE referencehash = ?";
2179          $rs = $DB->get_recordset_sql($sql, array($referencehash));
2180  
2181          $now = time();
2182          foreach ($rs as $record) {
2183              $this->update_references($record->id, $now, null,
2184                      $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2185          }
2186          $rs->close();
2187      }
2188  
2189      /**
2190       * Convert file alias to local file
2191       *
2192       * @throws moodle_exception if file could not be downloaded
2193       *
2194       * @param stored_file $storedfile a stored_file instances
2195       * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2196       * @return stored_file stored_file
2197       */
2198      public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2199          global $CFG;
2200          $storedfile->import_external_file_contents($maxbytes);
2201          $storedfile->delete_reference();
2202          return $storedfile;
2203      }
2204  
2205      /**
2206       * Return mimetype by given file pathname.
2207       *
2208       * If file has a known extension, we return the mimetype based on extension.
2209       * Otherwise (when possible) we try to get the mimetype from file contents.
2210       *
2211       * @param string $fullpath Full path to the file on disk
2212       * @param string $filename Correct file name with extension, if omitted will be taken from $path
2213       * @return string
2214       */
2215      public static function mimetype($fullpath, $filename = null) {
2216          if (empty($filename)) {
2217              $filename = $fullpath;
2218          }
2219  
2220          // The mimeinfo function determines the mimetype purely based on the file extension.
2221          $type = mimeinfo('type', $filename);
2222  
2223          if ($type === 'document/unknown') {
2224              // The type is unknown. Inspect the file now.
2225              $type = self::mimetype_from_file($fullpath);
2226          }
2227          return $type;
2228      }
2229  
2230      /**
2231       * Inspect a file on disk for it's mimetype.
2232       *
2233       * @param string $fullpath Path to file on disk
2234       * @return string The mimetype
2235       */
2236      public static function mimetype_from_file($fullpath) {
2237          if (file_exists($fullpath)) {
2238              // The type is unknown. Attempt to look up the file type now.
2239              $finfo = new finfo(FILEINFO_MIME_TYPE);
2240  
2241              // See https://bugs.php.net/bug.php?id=79045 - finfo isn't consistent with returned type, normalize into value
2242              // that is used internally by the {@see core_filetypes} class and the {@see mimeinfo_from_type} call below.
2243              $mimetype = $finfo->file($fullpath);
2244              if ($mimetype === 'image/svg') {
2245                  $mimetype = 'image/svg+xml';
2246              }
2247  
2248              return mimeinfo_from_type('type', $mimetype);
2249          }
2250  
2251          return 'document/unknown';
2252      }
2253  
2254      /**
2255       * Cron cleanup job.
2256       */
2257      public function cron() {
2258          global $CFG, $DB;
2259          require_once($CFG->libdir.'/cronlib.php');
2260  
2261          // find out all stale draft areas (older than 4 days) and purge them
2262          // those are identified by time stamp of the /. root dir
2263          mtrace('Deleting old draft files... ', '');
2264          cron_trace_time_and_memory();
2265          $old = time() - 60*60*24*4;
2266          $sql = "SELECT *
2267                    FROM {files}
2268                   WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2269                         AND timecreated < :old";
2270          $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2271          foreach ($rs as $dir) {
2272              $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2273          }
2274          $rs->close();
2275          mtrace('done.');
2276  
2277          // Remove orphaned files:
2278          // * preview files in the core preview filearea without the existing original file.
2279          // * document converted files in core documentconversion filearea without the existing original file.
2280          mtrace('Deleting orphaned preview, and document conversion files... ', '');
2281          cron_trace_time_and_memory();
2282          $sql = "SELECT p.*
2283                    FROM {files} p
2284               LEFT JOIN {files} o ON (p.filename = o.contenthash)
2285                   WHERE p.contextid = ?
2286                     AND p.component = 'core'
2287                     AND (p.filearea = 'preview' OR p.filearea = 'documentconversion')
2288                     AND p.itemid = 0
2289                     AND o.id IS NULL";
2290          $syscontext = context_system::instance();
2291          $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2292          foreach ($rs as $orphan) {
2293              $file = $this->get_file_instance($orphan);
2294              if (!$file->is_directory()) {
2295                  $file->delete();
2296              }
2297          }
2298          $rs->close();
2299          mtrace('done.');
2300  
2301          // remove trash pool files once a day
2302          // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2303          $filescleanupperiod = empty($CFG->filescleanupperiod) ? 86400 : $CFG->filescleanupperiod;
2304          if (empty($CFG->fileslastcleanup) || ($CFG->fileslastcleanup < time() - $filescleanupperiod)) {
2305              require_once($CFG->libdir.'/filelib.php');
2306              // Delete files that are associated with a context that no longer exists.
2307              mtrace('Cleaning up files from deleted contexts... ', '');
2308              cron_trace_time_and_memory();
2309              $sql = "SELECT DISTINCT f.contextid
2310                      FROM {files} f
2311                      LEFT OUTER JOIN {context} c ON f.contextid = c.id
2312                      WHERE c.id IS NULL";
2313              $rs = $DB->get_recordset_sql($sql);
2314              if ($rs->valid()) {
2315                  $fs = get_file_storage();
2316                  foreach ($rs as $ctx) {
2317                      $fs->delete_area_files($ctx->contextid);
2318                  }
2319              }
2320              $rs->close();
2321              mtrace('done.');
2322  
2323              mtrace('Call filesystem cron tasks.', '');
2324              cron_trace_time_and_memory();
2325              $this->filesystem->cron();
2326              mtrace('done.');
2327          }
2328      }
2329  
2330      /**
2331       * Get the sql formated fields for a file instance to be created from a
2332       * {files} and {files_refernece} join.
2333       *
2334       * @param string $filesprefix the table prefix for the {files} table
2335       * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2336       * @return string the sql to go after a SELECT
2337       */
2338      private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2339          // Note, these fieldnames MUST NOT overlap between the two tables,
2340          // else problems like MDL-33172 occur.
2341          $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2342              'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2343              'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2344  
2345          $referencefields = array('repositoryid' => 'repositoryid',
2346              'reference' => 'reference',
2347              'lastsync' => 'referencelastsync');
2348  
2349          // id is specifically named to prevent overlaping between the two tables.
2350          $fields = array();
2351          $fields[] = $filesprefix.'.id AS id';
2352          foreach ($filefields as $field) {
2353              $fields[] = "{$filesprefix}.{$field}";
2354          }
2355  
2356          foreach ($referencefields as $field => $alias) {
2357              $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2358          }
2359  
2360          return implode(', ', $fields);
2361      }
2362  
2363      /**
2364       * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2365       *
2366       * If the record already exists, its id is returned. If there is no such record yet,
2367       * new one is created (using the lastsync provided, too) and its id is returned.
2368       *
2369       * @param int $repositoryid
2370       * @param string $reference
2371       * @param int $lastsync
2372       * @param int $lifetime argument not used any more
2373       * @return int
2374       */
2375      private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2376          global $DB;
2377  
2378          $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2379  
2380          if ($id !== false) {
2381              // bah, that was easy
2382              return $id;
2383          }
2384  
2385          // no such record yet, create one
2386          try {
2387              $id = $DB->insert_record('files_reference', array(
2388                  'repositoryid'  => $repositoryid,
2389                  'reference'     => $reference,
2390                  'referencehash' => sha1($reference),
2391                  'lastsync'      => $lastsync));
2392          } catch (dml_exception $e) {
2393              // if inserting the new record failed, chances are that the race condition has just
2394              // occured and the unique index did not allow to create the second record with the same
2395              // repositoryid + reference combo
2396              $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2397          }
2398  
2399          return $id;
2400      }
2401  
2402      /**
2403       * Returns the id of the record in {files_reference} that matches the passed parameters
2404       *
2405       * Depending on the required strictness, false can be returned. The behaviour is consistent
2406       * with standard DML methods.
2407       *
2408       * @param int $repositoryid
2409       * @param string $reference
2410       * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2411       * @return int|bool
2412       */
2413      private function get_referencefileid($repositoryid, $reference, $strictness) {
2414          global $DB;
2415  
2416          return $DB->get_field('files_reference', 'id',
2417              array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2418      }
2419  
2420      /**
2421       * Updates a reference to the external resource and all files that use it
2422       *
2423       * This function is called after synchronisation of an external file and updates the
2424       * contenthash, filesize and status of all files that reference this external file
2425       * as well as time last synchronised.
2426       *
2427       * @param int $referencefileid
2428       * @param int $lastsync
2429       * @param int $lifetime argument not used any more, liefetime is returned by repository
2430       * @param string $contenthash
2431       * @param int $filesize
2432       * @param int $status 0 if ok or 666 if source is missing
2433       * @param int $timemodified last time modified of the source, if known
2434       */
2435      public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2436          global $DB;
2437          $referencefileid = clean_param($referencefileid, PARAM_INT);
2438          $lastsync = clean_param($lastsync, PARAM_INT);
2439          validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2440          $filesize = clean_param($filesize, PARAM_INT);
2441          $status = clean_param($status, PARAM_INT);
2442          $params = array('contenthash' => $contenthash,
2443                      'filesize' => $filesize,
2444                      'status' => $status,
2445                      'referencefileid' => $referencefileid,
2446                      'timemodified' => $timemodified);
2447          $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2448              status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2449              WHERE referencefileid = :referencefileid', $params);
2450          $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2451          $DB->update_record('files_reference', (object)$data);
2452      }
2453  
2454      /**
2455       * Calculate and return the contenthash of the supplied file.
2456       *
2457       * @param   string $filepath The path to the file on disk
2458       * @return  string The file's content hash
2459       */
2460      public static function hash_from_path($filepath) {
2461          return sha1_file($filepath);
2462      }
2463  
2464      /**
2465       * Calculate and return the contenthash of the supplied content.
2466       *
2467       * @param   string $content The file content
2468       * @return  string The file's content hash
2469       */
2470      public static function hash_from_string($content) {
2471          return sha1($content ?? '');
2472      }
2473  }