Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 and 311] [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($decoded); // hide E_NOTICE
    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  }