Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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