Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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