MDL-71126 Quiz: Manual grading page size preference can get stuck at 0
[moodle.git] / lib / filestorage / file_storage.php
blob65945ef7200651c03c33bfaf53591707224b05a5
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.
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/>.
18 /**
19 * Core file storage class definition.
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
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/stored_file.php");
30 /**
31 * File storage class used for low level access to stored files.
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.
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
45 class file_storage {
47 /** @var string tempdir */
48 private $tempdir;
50 /** @var file_system filesystem */
51 private $filesystem;
53 /**
54 * Constructor - do not use directly use {@link get_file_storage()} call instead.
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');
61 $this->setup_file_system();
64 /**
65 * Complete setup procedure for the file_system component.
67 * @return file_system
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;
80 $this->filesystem = new $class();
83 return $this->filesystem;
86 /**
87 * Return the file system instance.
89 * @return file_system
91 public function get_file_system() {
92 return $this->filesystem;
95 /**
96 * Calculates sha1 hash of unique full path name information.
98 * This hash is a unique file identifier - it is used to improve
99 * performance and overcome db index size limits.
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
109 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
110 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
114 * Does this file exist?
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
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);
128 if ($filename === '') {
129 $filename = '.';
132 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
133 return $this->file_exists_by_hash($pathnamehash);
137 * Whether or not the file exist
139 * @param string $pathnamehash path name hash
140 * @return bool
142 public function file_exists_by_hash($pathnamehash) {
143 global $DB;
145 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
149 * Create instance of file class from database record.
151 * @param stdClass $filerecord record from the files table left join files_reference table
152 * @return stored_file instance of file abstraction class
154 public function get_file_instance(stdClass $filerecord) {
155 $storedfile = new stored_file($this, $filerecord);
156 return $storedfile;
160 * Get converted document.
162 * Get an alternate version of the specified document, if it is possible to convert.
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
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);
174 return false;
178 * Verify the format is supported.
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.
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);
187 return false;
191 * Check if the installed version of unoconv is supported.
193 * @return bool true if the present version is supported, false otherwise.
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);
199 return false;
203 * Regenerate the test pdf and send it direct to the browser.
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);
209 return false;
213 * Check if unoconv configured path is correct and working.
215 * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
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);
221 return false;
225 * Returns an image file that represent the given stored file as a preview
227 * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
228 * future, the support for other mimetypes can be added, too (eg. generate an image
229 * preview of PDF, text documents etc).
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
235 public function get_file_preview(stored_file $file, $mode) {
237 $context = context_system::instance();
238 $path = '/' . trim($mode, '/') . '/';
239 $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
241 if (!$preview) {
242 $preview = $this->create_file_preview($file, $mode);
243 if (!$preview) {
244 return false;
248 return $preview;
252 * Return an available file name.
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...
257 * If the file name passed is available without modification, it is returned as is.
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
269 public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
270 global $DB;
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);
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;
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];
290 $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
291 if (isset($pathinfo['extension'])) {
292 $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
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);
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;
325 // Constructing the new filename.
326 $newfilename = $basename . ' (' . $number . ')';
327 if (isset($pathinfo['extension'])) {
328 $newfilename .= '.' . $pathinfo['extension'];
331 return $newfilename;
335 * Return an available directory name.
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...
340 * If the file path passed is available without modification, it is returned as is.
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
350 public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
351 global $DB;
353 // Ensure suggestedpath has trailing '/'
354 $suggestedpath = rtrim($suggestedpath, '/'). '/';
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;
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]. '/';
366 $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
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);
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;
396 return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
400 * Generates a preview image for the stored file
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
406 protected function create_file_preview(stored_file $file, $mode) {
408 $mimetype = $file->get_mimetype();
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);
414 } else {
415 // unable to create the preview of this mimetype yet
416 return false;
419 if (empty($data)) {
420 return false;
423 $context = context_system::instance();
424 $record = array(
425 'contextid' => $context->id,
426 'component' => 'core',
427 'filearea' => 'preview',
428 'itemid' => 0,
429 'filepath' => '/' . trim($mode, '/') . '/',
430 'filename' => $file->get_contenthash(),
433 $imageinfo = getimagesizefromstring($data);
434 if ($imageinfo) {
435 $record['mimetype'] = $imageinfo['mime'];
438 return $this->create_file_from_string($record, $data);
442 * Generates a preview for the stored image file
444 * @param stored_file $file the image we want to preview
445 * @param string $mode preview mode, eg. 'thumb'
446 * @return string|bool false if a problem occurs, the thumbnail image data otherwise
448 protected function create_imagefile_preview(stored_file $file, $mode) {
449 global $CFG;
450 require_once($CFG->libdir.'/gdlib.php');
452 if ($mode === 'tinyicon') {
453 $data = $file->generate_image_thumbnail(24, 24);
455 } else if ($mode === 'thumb') {
456 $data = $file->generate_image_thumbnail(90, 90);
458 } else if ($mode === 'bigthumb') {
459 $data = $file->generate_image_thumbnail(250, 250);
461 } else {
462 throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
465 return $data;
469 * Fetch file using local file id.
471 * Please do not rely on file ids, it is usually easier to use
472 * pathname hashes instead.
474 * @param int $fileid file ID
475 * @return stored_file|bool stored_file instance if exists, false if not
477 public function get_file_by_id($fileid) {
478 global $DB;
480 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
481 FROM {files} f
482 LEFT JOIN {files_reference} r
483 ON f.referencefileid = r.id
484 WHERE f.id = ?";
485 if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
486 return $this->get_file_instance($filerecord);
487 } else {
488 return false;
493 * Fetch file using local file full pathname hash
495 * @param string $pathnamehash path name hash
496 * @return stored_file|bool stored_file instance if exists, false if not
498 public function get_file_by_hash($pathnamehash) {
499 global $DB;
501 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
502 FROM {files} f
503 LEFT JOIN {files_reference} r
504 ON f.referencefileid = r.id
505 WHERE f.pathnamehash = ?";
506 if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
507 return $this->get_file_instance($filerecord);
508 } else {
509 return false;
514 * Fetch locally stored file.
516 * @param int $contextid context ID
517 * @param string $component component
518 * @param string $filearea file area
519 * @param int $itemid item ID
520 * @param string $filepath file path
521 * @param string $filename file name
522 * @return stored_file|bool stored_file instance if exists, false if not
524 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
525 $filepath = clean_param($filepath, PARAM_PATH);
526 $filename = clean_param($filename, PARAM_FILE);
528 if ($filename === '') {
529 $filename = '.';
532 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
533 return $this->get_file_by_hash($pathnamehash);
537 * Are there any files (or directories)
539 * @param int $contextid context ID
540 * @param string $component component
541 * @param string $filearea file area
542 * @param bool|int $itemid item id or false if all items
543 * @param bool $ignoredirs whether or not ignore directories
544 * @return bool empty
546 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
547 global $DB;
549 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
550 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
552 if ($itemid !== false) {
553 $params['itemid'] = $itemid;
554 $where .= " AND itemid = :itemid";
557 if ($ignoredirs) {
558 $sql = "SELECT 'x'
559 FROM {files}
560 WHERE $where AND filename <> '.'";
561 } else {
562 $sql = "SELECT 'x'
563 FROM {files}
564 WHERE $where AND (filename <> '.' OR filepath <> '/')";
567 return !$DB->record_exists_sql($sql, $params);
571 * Returns all files belonging to given repository
573 * @param int $repositoryid
574 * @param string $sort A fragment of SQL to use for sorting
576 public function get_external_files($repositoryid, $sort = '') {
577 global $DB;
578 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
579 FROM {files} f
580 LEFT JOIN {files_reference} r
581 ON f.referencefileid = r.id
582 WHERE r.repositoryid = ?";
583 if (!empty($sort)) {
584 $sql .= " ORDER BY {$sort}";
587 $result = array();
588 $filerecords = $DB->get_records_sql($sql, array($repositoryid));
589 foreach ($filerecords as $filerecord) {
590 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
592 return $result;
596 * Returns all area files (optionally limited by itemid)
598 * @param int $contextid context ID
599 * @param string $component component
600 * @param mixed $filearea file area/s, you cannot specify multiple fileareas as well as an itemid
601 * @param int|int[]|false $itemid item ID(s) or all files if not specified
602 * @param string $sort A fragment of SQL to use for sorting
603 * @param bool $includedirs whether or not include directories
604 * @param int $updatedsince return files updated since this time
605 * @param int $limitfrom return a subset of records, starting at this point (optional).
606 * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
607 * @return stored_file[] array of stored_files indexed by pathanmehash
609 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename",
610 $includedirs = true, $updatedsince = 0, $limitfrom = 0, $limitnum = 0) {
611 global $DB;
613 list($areasql, $conditions) = $DB->get_in_or_equal($filearea, SQL_PARAMS_NAMED);
614 $conditions['contextid'] = $contextid;
615 $conditions['component'] = $component;
617 if ($itemid !== false && is_array($filearea)) {
618 throw new coding_exception('You cannot specify multiple fileareas as well as an itemid.');
619 } else if ($itemid !== false) {
620 $itemids = is_array($itemid) ? $itemid : [$itemid];
621 list($itemidinorequalsql, $itemidconditions) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
622 $itemidsql = " AND f.itemid {$itemidinorequalsql}";
623 $conditions = array_merge($conditions, $itemidconditions);
624 } else {
625 $itemidsql = '';
628 $updatedsincesql = '';
629 if (!empty($updatedsince)) {
630 $conditions['time'] = $updatedsince;
631 $updatedsincesql = 'AND f.timemodified > :time';
634 $includedirssql = '';
635 if (!$includedirs) {
636 $includedirssql = 'AND f.filename != :dot';
637 $conditions['dot'] = '.';
640 if ($limitfrom && !$limitnum) {
641 throw new coding_exception('If specifying $limitfrom you must also specify $limitnum');
644 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
645 FROM {files} f
646 LEFT JOIN {files_reference} r
647 ON f.referencefileid = r.id
648 WHERE f.contextid = :contextid
649 AND f.component = :component
650 AND f.filearea $areasql
651 $includedirssql
652 $updatedsincesql
653 $itemidsql";
654 if (!empty($sort)) {
655 $sql .= " ORDER BY {$sort}";
658 $result = array();
659 $filerecords = $DB->get_records_sql($sql, $conditions, $limitfrom, $limitnum);
660 foreach ($filerecords as $filerecord) {
661 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
663 return $result;
667 * Returns the file area item ids and their updatetime for a user's draft uploads, sorted by updatetime DESC.
669 * @param int $userid user id
670 * @param int $updatedsince only return draft areas updated since this time
671 * @param int $lastnum only return the last specified numbers
672 * @return array
674 public function get_user_draft_items(int $userid, int $updatedsince = 0, int $lastnum = 0): array {
675 global $DB;
677 $params = [
678 'component' => 'user',
679 'filearea' => 'draft',
680 'contextid' => context_user::instance($userid)->id,
683 $updatedsincesql = '';
684 if ($updatedsince) {
685 $updatedsincesql = 'AND f.timemodified > :time';
686 $params['time'] = $updatedsince;
688 $sql = "SELECT itemid,
689 MAX(f.timemodified) AS timemodified
690 FROM {files} f
691 WHERE component = :component
692 AND filearea = :filearea
693 AND contextid = :contextid
694 $updatedsincesql
695 GROUP BY itemid
696 ORDER BY MAX(f.timemodified) DESC";
698 return $DB->get_records_sql($sql, $params, 0, $lastnum);
702 * Returns array based tree structure of area files
704 * @param int $contextid context ID
705 * @param string $component component
706 * @param string $filearea file area
707 * @param int $itemid item ID
708 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
710 public function get_area_tree($contextid, $component, $filearea, $itemid) {
711 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
712 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
713 // first create directory structure
714 foreach ($files as $hash=>$dir) {
715 if (!$dir->is_directory()) {
716 continue;
718 unset($files[$hash]);
719 if ($dir->get_filepath() === '/') {
720 $result['dirfile'] = $dir;
721 continue;
723 $parts = explode('/', trim($dir->get_filepath(),'/'));
724 $pointer =& $result;
725 foreach ($parts as $part) {
726 if ($part === '') {
727 continue;
729 if (!isset($pointer['subdirs'][$part])) {
730 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
732 $pointer =& $pointer['subdirs'][$part];
734 $pointer['dirfile'] = $dir;
735 unset($pointer);
737 foreach ($files as $hash=>$file) {
738 $parts = explode('/', trim($file->get_filepath(),'/'));
739 $pointer =& $result;
740 foreach ($parts as $part) {
741 if ($part === '') {
742 continue;
744 $pointer =& $pointer['subdirs'][$part];
746 $pointer['files'][$file->get_filename()] = $file;
747 unset($pointer);
749 $result = $this->sort_area_tree($result);
750 return $result;
754 * Sorts the result of {@link file_storage::get_area_tree()}.
756 * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
757 * @return array of sorted results
759 protected function sort_area_tree($tree) {
760 foreach ($tree as $key => &$value) {
761 if ($key == 'subdirs') {
762 core_collator::ksort($value, core_collator::SORT_NATURAL);
763 foreach ($value as $subdirname => &$subtree) {
764 $subtree = $this->sort_area_tree($subtree);
766 } else if ($key == 'files') {
767 core_collator::ksort($value, core_collator::SORT_NATURAL);
770 return $tree;
774 * Returns all files and optionally directories
776 * @param int $contextid context ID
777 * @param string $component component
778 * @param string $filearea file area
779 * @param int $itemid item ID
780 * @param int $filepath directory path
781 * @param bool $recursive include all subdirectories
782 * @param bool $includedirs include files and directories
783 * @param string $sort A fragment of SQL to use for sorting
784 * @return array of stored_files indexed by pathanmehash
786 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
787 global $DB;
789 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
790 return array();
793 $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
795 if ($recursive) {
797 $dirs = $includedirs ? "" : "AND filename <> '.'";
798 $length = core_text::strlen($filepath);
800 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
801 FROM {files} f
802 LEFT JOIN {files_reference} r
803 ON f.referencefileid = r.id
804 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
805 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
806 AND f.id <> :dirid
807 $dirs
808 $orderby";
809 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
811 $files = array();
812 $dirs = array();
813 $filerecords = $DB->get_records_sql($sql, $params);
814 foreach ($filerecords as $filerecord) {
815 if ($filerecord->filename == '.') {
816 $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
817 } else {
818 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
821 $result = array_merge($dirs, $files);
823 } else {
824 $result = array();
825 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
827 $length = core_text::strlen($filepath);
829 if ($includedirs) {
830 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
831 FROM {files} f
832 LEFT JOIN {files_reference} r
833 ON f.referencefileid = r.id
834 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
835 AND f.itemid = :itemid AND f.filename = '.'
836 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
837 AND f.id <> :dirid
838 $orderby";
839 $reqlevel = substr_count($filepath, '/') + 1;
840 $filerecords = $DB->get_records_sql($sql, $params);
841 foreach ($filerecords as $filerecord) {
842 if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
843 continue;
845 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
849 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
850 FROM {files} f
851 LEFT JOIN {files_reference} r
852 ON f.referencefileid = r.id
853 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
854 AND f.filepath = :filepath AND f.filename <> '.'
855 $orderby";
857 $filerecords = $DB->get_records_sql($sql, $params);
858 foreach ($filerecords as $filerecord) {
859 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
863 return $result;
867 * Delete all area files (optionally limited by itemid).
869 * @param int $contextid context ID
870 * @param string $component component
871 * @param string $filearea file area or all areas in context if not specified
872 * @param int $itemid item ID or all files if not specified
873 * @return bool success
875 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
876 global $DB;
878 $conditions = array('contextid'=>$contextid);
879 if ($component !== false) {
880 $conditions['component'] = $component;
882 if ($filearea !== false) {
883 $conditions['filearea'] = $filearea;
885 if ($itemid !== false) {
886 $conditions['itemid'] = $itemid;
889 $filerecords = $DB->get_records('files', $conditions);
890 foreach ($filerecords as $filerecord) {
891 $this->get_file_instance($filerecord)->delete();
894 return true; // BC only
898 * Delete all the files from certain areas where itemid is limited by an
899 * arbitrary bit of SQL.
901 * @param int $contextid the id of the context the files belong to. Must be given.
902 * @param string $component the owning component. Must be given.
903 * @param string $filearea the file area name. Must be given.
904 * @param string $itemidstest an SQL fragment that the itemid must match. Used
905 * in the query like WHERE itemid $itemidstest. Must used named parameters,
906 * and may not used named parameters called contextid, component or filearea.
907 * @param array $params any query params used by $itemidstest.
909 public function delete_area_files_select($contextid, $component,
910 $filearea, $itemidstest, array $params = null) {
911 global $DB;
913 $where = "contextid = :contextid
914 AND component = :component
915 AND filearea = :filearea
916 AND itemid $itemidstest";
917 $params['contextid'] = $contextid;
918 $params['component'] = $component;
919 $params['filearea'] = $filearea;
921 $filerecords = $DB->get_recordset_select('files', $where, $params);
922 foreach ($filerecords as $filerecord) {
923 $this->get_file_instance($filerecord)->delete();
925 $filerecords->close();
929 * Delete all files associated with the given component.
931 * @param string $component the component owning the file
933 public function delete_component_files($component) {
934 global $DB;
936 $filerecords = $DB->get_recordset('files', array('component' => $component));
937 foreach ($filerecords as $filerecord) {
938 $this->get_file_instance($filerecord)->delete();
940 $filerecords->close();
944 * Move all the files in a file area from one context to another.
946 * @param int $oldcontextid the context the files are being moved from.
947 * @param int $newcontextid the context the files are being moved to.
948 * @param string $component the plugin that these files belong to.
949 * @param string $filearea the name of the file area.
950 * @param int $itemid file item ID
951 * @return int the number of files moved, for information.
953 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
954 // Note, this code is based on some code that Petr wrote in
955 // forum_move_attachments in mod/forum/lib.php. I moved it here because
956 // I needed it in the question code too.
957 $count = 0;
959 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
960 foreach ($oldfiles as $oldfile) {
961 $filerecord = new stdClass();
962 $filerecord->contextid = $newcontextid;
963 $this->create_file_from_storedfile($filerecord, $oldfile);
964 $count += 1;
967 if ($count) {
968 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
971 return $count;
975 * Recursively creates directory.
977 * @param int $contextid context ID
978 * @param string $component component
979 * @param string $filearea file area
980 * @param int $itemid item ID
981 * @param string $filepath file path
982 * @param int $userid the user ID
983 * @return bool success
985 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
986 global $DB;
988 // validate all parameters, we do not want any rubbish stored in database, right?
989 if (!is_number($contextid) or $contextid < 1) {
990 throw new file_exception('storedfileproblem', 'Invalid contextid');
993 $component = clean_param($component, PARAM_COMPONENT);
994 if (empty($component)) {
995 throw new file_exception('storedfileproblem', 'Invalid component');
998 $filearea = clean_param($filearea, PARAM_AREA);
999 if (empty($filearea)) {
1000 throw new file_exception('storedfileproblem', 'Invalid filearea');
1003 if (!is_number($itemid) or $itemid < 0) {
1004 throw new file_exception('storedfileproblem', 'Invalid itemid');
1007 $filepath = clean_param($filepath, PARAM_PATH);
1008 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1009 // path must start and end with '/'
1010 throw new file_exception('storedfileproblem', 'Invalid file path');
1013 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1015 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1016 return $dir_info;
1019 static $contenthash = null;
1020 if (!$contenthash) {
1021 $this->add_string_to_pool('');
1022 $contenthash = self::hash_from_string('');
1025 $now = time();
1027 $dir_record = new stdClass();
1028 $dir_record->contextid = $contextid;
1029 $dir_record->component = $component;
1030 $dir_record->filearea = $filearea;
1031 $dir_record->itemid = $itemid;
1032 $dir_record->filepath = $filepath;
1033 $dir_record->filename = '.';
1034 $dir_record->contenthash = $contenthash;
1035 $dir_record->filesize = 0;
1037 $dir_record->timecreated = $now;
1038 $dir_record->timemodified = $now;
1039 $dir_record->mimetype = null;
1040 $dir_record->userid = $userid;
1042 $dir_record->pathnamehash = $pathnamehash;
1044 $DB->insert_record('files', $dir_record);
1045 $dir_info = $this->get_file_by_hash($pathnamehash);
1047 if ($filepath !== '/') {
1048 //recurse to parent dirs
1049 $filepath = trim($filepath, '/');
1050 $filepath = explode('/', $filepath);
1051 array_pop($filepath);
1052 $filepath = implode('/', $filepath);
1053 $filepath = ($filepath === '') ? '/' : "/$filepath/";
1054 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1057 return $dir_info;
1061 * Add new file record to database and handle callbacks.
1063 * @param stdClass $newrecord
1065 protected function create_file($newrecord) {
1066 global $DB;
1067 $newrecord->id = $DB->insert_record('files', $newrecord);
1069 if ($newrecord->filename !== '.') {
1070 // Callback for file created.
1071 if ($pluginsfunction = get_plugins_with_function('after_file_created')) {
1072 foreach ($pluginsfunction as $plugintype => $plugins) {
1073 foreach ($plugins as $pluginfunction) {
1074 $pluginfunction($newrecord);
1082 * Add new local file based on existing local file.
1084 * @param stdClass|array $filerecord object or array describing changes
1085 * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1086 * @return stored_file instance of newly created file
1088 public function create_file_from_storedfile($filerecord, $fileorid) {
1089 global $DB;
1091 if ($fileorid instanceof stored_file) {
1092 $fid = $fileorid->get_id();
1093 } else {
1094 $fid = $fileorid;
1097 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1099 unset($filerecord['id']);
1100 unset($filerecord['filesize']);
1101 unset($filerecord['contenthash']);
1102 unset($filerecord['pathnamehash']);
1104 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1105 FROM {files} f
1106 LEFT JOIN {files_reference} r
1107 ON f.referencefileid = r.id
1108 WHERE f.id = ?";
1110 if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1111 throw new file_exception('storedfileproblem', 'File does not exist');
1114 unset($newrecord->id);
1116 foreach ($filerecord as $key => $value) {
1117 // validate all parameters, we do not want any rubbish stored in database, right?
1118 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1119 throw new file_exception('storedfileproblem', 'Invalid contextid');
1122 if ($key == 'component') {
1123 $value = clean_param($value, PARAM_COMPONENT);
1124 if (empty($value)) {
1125 throw new file_exception('storedfileproblem', 'Invalid component');
1129 if ($key == 'filearea') {
1130 $value = clean_param($value, PARAM_AREA);
1131 if (empty($value)) {
1132 throw new file_exception('storedfileproblem', 'Invalid filearea');
1136 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1137 throw new file_exception('storedfileproblem', 'Invalid itemid');
1141 if ($key == 'filepath') {
1142 $value = clean_param($value, PARAM_PATH);
1143 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1144 // path must start and end with '/'
1145 throw new file_exception('storedfileproblem', 'Invalid file path');
1149 if ($key == 'filename') {
1150 $value = clean_param($value, PARAM_FILE);
1151 if ($value === '') {
1152 // path must start and end with '/'
1153 throw new file_exception('storedfileproblem', 'Invalid file name');
1157 if ($key === 'timecreated' or $key === 'timemodified') {
1158 if (!is_number($value)) {
1159 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1161 if ($value < 0) {
1162 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1163 $value = 0;
1167 if ($key == 'referencefileid' or $key == 'referencelastsync') {
1168 $value = clean_param($value, PARAM_INT);
1171 $newrecord->$key = $value;
1174 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1176 if ($newrecord->filename === '.') {
1177 // special case - only this function supports directories ;-)
1178 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1179 // update the existing directory with the new data
1180 $newrecord->id = $directory->get_id();
1181 $DB->update_record('files', $newrecord);
1182 return $this->get_file_instance($newrecord);
1185 // note: referencefileid is copied from the original file so that
1186 // creating a new file from an existing alias creates new alias implicitly.
1187 // here we just check the database consistency.
1188 if (!empty($newrecord->repositoryid)) {
1189 // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files
1190 // where saved from a draft area.
1191 $newrecord->referencefileid = $this->get_or_create_referencefileid($newrecord->repositoryid, $newrecord->reference);
1194 try {
1195 $this->create_file($newrecord);
1196 } catch (dml_exception $e) {
1197 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1198 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1202 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1204 return $this->get_file_instance($newrecord);
1208 * Add new local file.
1210 * @param stdClass|array $filerecord object or array describing file
1211 * @param string $url the URL to the file
1212 * @param array $options {@link download_file_content()} options
1213 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1214 * @return stored_file
1216 public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1218 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1219 $filerecord = (object)$filerecord; // We support arrays too.
1221 $headers = isset($options['headers']) ? $options['headers'] : null;
1222 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
1223 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
1224 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
1225 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1226 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1227 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
1229 if (!isset($filerecord->filename)) {
1230 $parts = explode('/', $url);
1231 $filename = array_pop($parts);
1232 $filerecord->filename = clean_param($filename, PARAM_FILE);
1234 $source = !empty($filerecord->source) ? $filerecord->source : $url;
1235 $filerecord->source = clean_param($source, PARAM_URL);
1237 if ($usetempfile) {
1238 check_dir_exists($this->tempdir);
1239 $tmpfile = tempnam($this->tempdir, 'newfromurl');
1240 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1241 if ($content === false) {
1242 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1244 try {
1245 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1246 @unlink($tmpfile);
1247 return $newfile;
1248 } catch (Exception $e) {
1249 @unlink($tmpfile);
1250 throw $e;
1253 } else {
1254 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1255 if ($content === false) {
1256 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1258 return $this->create_file_from_string($filerecord, $content);
1263 * Add new local file.
1265 * @param stdClass|array $filerecord object or array describing file
1266 * @param string $pathname path to file or content of file
1267 * @return stored_file
1269 public function create_file_from_pathname($filerecord, $pathname) {
1270 global $DB;
1272 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1273 $filerecord = (object)$filerecord; // We support arrays too.
1275 // validate all parameters, we do not want any rubbish stored in database, right?
1276 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1277 throw new file_exception('storedfileproblem', 'Invalid contextid');
1280 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1281 if (empty($filerecord->component)) {
1282 throw new file_exception('storedfileproblem', 'Invalid component');
1285 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1286 if (empty($filerecord->filearea)) {
1287 throw new file_exception('storedfileproblem', 'Invalid filearea');
1290 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1291 throw new file_exception('storedfileproblem', 'Invalid itemid');
1294 if (!empty($filerecord->sortorder)) {
1295 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1296 $filerecord->sortorder = 0;
1298 } else {
1299 $filerecord->sortorder = 0;
1302 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1303 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1304 // path must start and end with '/'
1305 throw new file_exception('storedfileproblem', 'Invalid file path');
1308 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1309 if ($filerecord->filename === '') {
1310 // filename must not be empty
1311 throw new file_exception('storedfileproblem', 'Invalid file name');
1314 $now = time();
1315 if (isset($filerecord->timecreated)) {
1316 if (!is_number($filerecord->timecreated)) {
1317 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1319 if ($filerecord->timecreated < 0) {
1320 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1321 $filerecord->timecreated = 0;
1323 } else {
1324 $filerecord->timecreated = $now;
1327 if (isset($filerecord->timemodified)) {
1328 if (!is_number($filerecord->timemodified)) {
1329 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1331 if ($filerecord->timemodified < 0) {
1332 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1333 $filerecord->timemodified = 0;
1335 } else {
1336 $filerecord->timemodified = $now;
1339 $newrecord = new stdClass();
1341 $newrecord->contextid = $filerecord->contextid;
1342 $newrecord->component = $filerecord->component;
1343 $newrecord->filearea = $filerecord->filearea;
1344 $newrecord->itemid = $filerecord->itemid;
1345 $newrecord->filepath = $filerecord->filepath;
1346 $newrecord->filename = $filerecord->filename;
1348 $newrecord->timecreated = $filerecord->timecreated;
1349 $newrecord->timemodified = $filerecord->timemodified;
1350 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1351 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1352 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1353 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1354 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1355 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1356 $newrecord->sortorder = $filerecord->sortorder;
1358 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname, null, $newrecord);
1360 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1362 try {
1363 $this->create_file($newrecord);
1364 } catch (dml_exception $e) {
1365 if ($newfile) {
1366 $this->filesystem->remove_file($newrecord->contenthash);
1368 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1369 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1372 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1374 return $this->get_file_instance($newrecord);
1378 * Add new local file.
1380 * @param stdClass|array $filerecord object or array describing file
1381 * @param string $content content of file
1382 * @return stored_file
1384 public function create_file_from_string($filerecord, $content) {
1385 global $DB;
1387 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1388 $filerecord = (object)$filerecord; // We support arrays too.
1390 // validate all parameters, we do not want any rubbish stored in database, right?
1391 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1392 throw new file_exception('storedfileproblem', 'Invalid contextid');
1395 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1396 if (empty($filerecord->component)) {
1397 throw new file_exception('storedfileproblem', 'Invalid component');
1400 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1401 if (empty($filerecord->filearea)) {
1402 throw new file_exception('storedfileproblem', 'Invalid filearea');
1405 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1406 throw new file_exception('storedfileproblem', 'Invalid itemid');
1409 if (!empty($filerecord->sortorder)) {
1410 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1411 $filerecord->sortorder = 0;
1413 } else {
1414 $filerecord->sortorder = 0;
1417 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1418 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1419 // path must start and end with '/'
1420 throw new file_exception('storedfileproblem', 'Invalid file path');
1423 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1424 if ($filerecord->filename === '') {
1425 // path must start and end with '/'
1426 throw new file_exception('storedfileproblem', 'Invalid file name');
1429 $now = time();
1430 if (isset($filerecord->timecreated)) {
1431 if (!is_number($filerecord->timecreated)) {
1432 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1434 if ($filerecord->timecreated < 0) {
1435 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1436 $filerecord->timecreated = 0;
1438 } else {
1439 $filerecord->timecreated = $now;
1442 if (isset($filerecord->timemodified)) {
1443 if (!is_number($filerecord->timemodified)) {
1444 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1446 if ($filerecord->timemodified < 0) {
1447 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1448 $filerecord->timemodified = 0;
1450 } else {
1451 $filerecord->timemodified = $now;
1454 $newrecord = new stdClass();
1456 $newrecord->contextid = $filerecord->contextid;
1457 $newrecord->component = $filerecord->component;
1458 $newrecord->filearea = $filerecord->filearea;
1459 $newrecord->itemid = $filerecord->itemid;
1460 $newrecord->filepath = $filerecord->filepath;
1461 $newrecord->filename = $filerecord->filename;
1463 $newrecord->timecreated = $filerecord->timecreated;
1464 $newrecord->timemodified = $filerecord->timemodified;
1465 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1466 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1467 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1468 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1469 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1470 $newrecord->sortorder = $filerecord->sortorder;
1472 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content, $newrecord);
1473 if (empty($filerecord->mimetype)) {
1474 $newrecord->mimetype = $this->filesystem->mimetype_from_hash($newrecord->contenthash, $newrecord->filename);
1475 } else {
1476 $newrecord->mimetype = $filerecord->mimetype;
1479 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1481 try {
1482 $this->create_file($newrecord);
1483 } catch (dml_exception $e) {
1484 if ($newfile) {
1485 $this->filesystem->remove_file($newrecord->contenthash);
1487 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1488 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1491 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1493 return $this->get_file_instance($newrecord);
1497 * Synchronise stored file from file.
1499 * @param stored_file $file Stored file to synchronise.
1500 * @param string $path Path to the file to synchronise from.
1501 * @param stdClass $filerecord The file record from the database.
1503 public function synchronise_stored_file_from_file(stored_file $file, $path, $filerecord) {
1504 list($contenthash, $filesize) = $this->add_file_to_pool($path, null, $filerecord);
1505 $file->set_synchronized($contenthash, $filesize);
1509 * Synchronise stored file from string.
1511 * @param stored_file $file Stored file to synchronise.
1512 * @param string $content File content.
1513 * @param stdClass $filerecord The file record from the database.
1515 public function synchronise_stored_file_from_string(stored_file $file, $content, $filerecord) {
1516 list($contenthash, $filesize) = $this->add_string_to_pool($content, $filerecord);
1517 $file->set_synchronized($contenthash, $filesize);
1521 * Create a new alias/shortcut file from file reference information
1523 * @param stdClass|array $filerecord object or array describing the new file
1524 * @param int $repositoryid the id of the repository that provides the original file
1525 * @param string $reference the information required by the repository to locate the original file
1526 * @param array $options options for creating the new file
1527 * @return stored_file
1529 public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1530 global $DB;
1532 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1533 $filerecord = (object)$filerecord; // We support arrays too.
1535 // validate all parameters, we do not want any rubbish stored in database, right?
1536 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1537 throw new file_exception('storedfileproblem', 'Invalid contextid');
1540 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1541 if (empty($filerecord->component)) {
1542 throw new file_exception('storedfileproblem', 'Invalid component');
1545 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1546 if (empty($filerecord->filearea)) {
1547 throw new file_exception('storedfileproblem', 'Invalid filearea');
1550 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1551 throw new file_exception('storedfileproblem', 'Invalid itemid');
1554 if (!empty($filerecord->sortorder)) {
1555 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1556 $filerecord->sortorder = 0;
1558 } else {
1559 $filerecord->sortorder = 0;
1562 $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1563 $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1564 $filerecord->source = empty($filerecord->source) ? null : $filerecord->source;
1565 $filerecord->author = empty($filerecord->author) ? null : $filerecord->author;
1566 $filerecord->license = empty($filerecord->license) ? null : $filerecord->license;
1567 $filerecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1568 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1569 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1570 // Path must start and end with '/'.
1571 throw new file_exception('storedfileproblem', 'Invalid file path');
1574 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1575 if ($filerecord->filename === '') {
1576 // Path must start and end with '/'.
1577 throw new file_exception('storedfileproblem', 'Invalid file name');
1580 $now = time();
1581 if (isset($filerecord->timecreated)) {
1582 if (!is_number($filerecord->timecreated)) {
1583 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1585 if ($filerecord->timecreated < 0) {
1586 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1587 $filerecord->timecreated = 0;
1589 } else {
1590 $filerecord->timecreated = $now;
1593 if (isset($filerecord->timemodified)) {
1594 if (!is_number($filerecord->timemodified)) {
1595 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1597 if ($filerecord->timemodified < 0) {
1598 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1599 $filerecord->timemodified = 0;
1601 } else {
1602 $filerecord->timemodified = $now;
1605 $transaction = $DB->start_delegated_transaction();
1607 try {
1608 $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1609 } catch (Exception $e) {
1610 throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1613 $existingfile = null;
1614 if (isset($filerecord->contenthash)) {
1615 $existingfile = $DB->get_record('files', array('contenthash' => $filerecord->contenthash), '*', IGNORE_MULTIPLE);
1617 if (!empty($existingfile)) {
1618 // There is an existing file already available.
1619 if (empty($filerecord->filesize)) {
1620 $filerecord->filesize = $existingfile->filesize;
1621 } else {
1622 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1624 } else {
1625 // Attempt to get the result of last synchronisation for this reference.
1626 $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1627 'id, contenthash, filesize', IGNORE_MULTIPLE);
1628 if ($lastcontent) {
1629 $filerecord->contenthash = $lastcontent->contenthash;
1630 $filerecord->filesize = $lastcontent->filesize;
1631 } else {
1632 // External file doesn't have content in moodle.
1633 // So we create an empty file for it.
1634 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null, $filerecord);
1638 $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1640 try {
1641 $filerecord->id = $DB->insert_record('files', $filerecord);
1642 } catch (dml_exception $e) {
1643 if (!empty($newfile)) {
1644 $this->filesystem->remove_file($filerecord->contenthash);
1646 throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1647 $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1650 $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1652 $transaction->allow_commit();
1654 // this will retrieve all reference information from DB as well
1655 return $this->get_file_by_id($filerecord->id);
1659 * Creates new image file from existing.
1661 * @param stdClass|array $filerecord object or array describing new file
1662 * @param int|stored_file $fid file id or stored file object
1663 * @param int $newwidth in pixels
1664 * @param int $newheight in pixels
1665 * @param bool $keepaspectratio whether or not keep aspect ratio
1666 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1667 * @return stored_file
1669 public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1670 if (!function_exists('imagecreatefromstring')) {
1671 //Most likely the GD php extension isn't installed
1672 //image conversion cannot succeed
1673 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1676 if ($fid instanceof stored_file) {
1677 $fid = $fid->get_id();
1680 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1682 if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1683 throw new file_exception('storedfileproblem', 'File does not exist');
1686 if (!$imageinfo = $file->get_imageinfo()) {
1687 throw new file_exception('storedfileproblem', 'File is not an image');
1690 if (!isset($filerecord['filename'])) {
1691 $filerecord['filename'] = $file->get_filename();
1694 if (!isset($filerecord['mimetype'])) {
1695 $filerecord['mimetype'] = $imageinfo['mimetype'];
1698 $width = $imageinfo['width'];
1699 $height = $imageinfo['height'];
1701 if ($keepaspectratio) {
1702 if (0 >= $newwidth and 0 >= $newheight) {
1703 // no sizes specified
1704 $newwidth = $width;
1705 $newheight = $height;
1707 } else if (0 < $newwidth and 0 < $newheight) {
1708 $xheight = ($newwidth*($height/$width));
1709 if ($xheight < $newheight) {
1710 $newheight = (int)$xheight;
1711 } else {
1712 $newwidth = (int)($newheight*($width/$height));
1715 } else if (0 < $newwidth) {
1716 $newheight = (int)($newwidth*($height/$width));
1718 } else { //0 < $newheight
1719 $newwidth = (int)($newheight*($width/$height));
1722 } else {
1723 if (0 >= $newwidth) {
1724 $newwidth = $width;
1726 if (0 >= $newheight) {
1727 $newheight = $height;
1731 // The original image.
1732 $img = imagecreatefromstring($file->get_content());
1734 // A new true color image where we will copy our original image.
1735 $newimg = imagecreatetruecolor($newwidth, $newheight);
1737 // Determine if the file supports transparency.
1738 $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1740 // Maintain transparency.
1741 if ($hasalpha) {
1742 imagealphablending($newimg, true);
1744 // Get the current transparent index for the original image.
1745 $colour = imagecolortransparent($img);
1746 if ($colour == -1) {
1747 // Set a transparent colour index if there's none.
1748 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1749 // Save full alpha channel.
1750 imagesavealpha($newimg, true);
1752 imagecolortransparent($newimg, $colour);
1753 imagefill($newimg, 0, 0, $colour);
1756 // Process the image to be output.
1757 if ($height != $newheight or $width != $newwidth) {
1758 // Resample if the dimensions differ from the original.
1759 if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1760 // weird
1761 throw new file_exception('storedfileproblem', 'Can not resize image');
1763 imagedestroy($img);
1764 $img = $newimg;
1766 } else if ($hasalpha) {
1767 // Just copy to the new image with the alpha channel.
1768 if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1769 // Weird.
1770 throw new file_exception('storedfileproblem', 'Can not copy image');
1772 imagedestroy($img);
1773 $img = $newimg;
1775 } else {
1776 // No particular processing needed for the original image.
1777 imagedestroy($newimg);
1780 ob_start();
1781 switch ($filerecord['mimetype']) {
1782 case 'image/gif':
1783 imagegif($img);
1784 break;
1786 case 'image/jpeg':
1787 if (is_null($quality)) {
1788 imagejpeg($img);
1789 } else {
1790 imagejpeg($img, NULL, $quality);
1792 break;
1794 case 'image/png':
1795 $quality = (int)$quality;
1797 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1798 // the latter of which can go to 100, we need to make sure that quality here is
1799 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1800 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1801 imagepng($img, NULL, $quality, NULL);
1802 break;
1804 default:
1805 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1808 $content = ob_get_contents();
1809 ob_end_clean();
1810 imagedestroy($img);
1812 if (!$content) {
1813 throw new file_exception('storedfileproblem', 'Can not convert image');
1816 return $this->create_file_from_string($filerecord, $content);
1820 * Add file content to sha1 pool.
1822 * @param string $pathname path to file
1823 * @param string|null $contenthash sha1 hash of content if known (performance only)
1824 * @param stdClass|null $newrecord New file record
1825 * @return array (contenthash, filesize, newfile)
1827 public function add_file_to_pool($pathname, $contenthash = null, $newrecord = null) {
1828 $this->call_before_file_created_plugin_functions($newrecord, $pathname);
1829 return $this->filesystem->add_file_from_path($pathname, $contenthash);
1833 * Add string content to sha1 pool.
1835 * @param string $content file content - binary string
1836 * @return array (contenthash, filesize, newfile)
1838 public function add_string_to_pool($content, $newrecord = null) {
1839 $this->call_before_file_created_plugin_functions($newrecord, null, $content);
1840 return $this->filesystem->add_file_from_string($content);
1844 * before_file_created hook.
1846 * @param stdClass|null $newrecord New file record.
1847 * @param string|null $pathname Path to file.
1848 * @param string|null $content File content.
1850 protected function call_before_file_created_plugin_functions($newrecord, $pathname = null, $content = null) {
1851 $pluginsfunction = get_plugins_with_function('before_file_created');
1852 foreach ($pluginsfunction as $plugintype => $plugins) {
1853 foreach ($plugins as $pluginfunction) {
1854 $pluginfunction($newrecord, ['pathname' => $pathname, 'content' => $content]);
1860 * Serve file content using X-Sendfile header.
1861 * Please make sure that all headers are already sent and the all
1862 * access control checks passed.
1864 * This alternate method to xsendfile() allows an alternate file system
1865 * to use the full file metadata and avoid extra lookups.
1867 * @param stored_file $file The file to send
1868 * @return bool success
1870 public function xsendfile_file(stored_file $file): bool {
1871 return $this->filesystem->xsendfile_file($file);
1875 * Serve file content using X-Sendfile header.
1876 * Please make sure that all headers are already sent
1877 * and the all access control checks passed.
1879 * @param string $contenthash sah1 hash of the file content to be served
1880 * @return bool success
1882 public function xsendfile($contenthash) {
1883 return $this->filesystem->xsendfile($contenthash);
1887 * Returns true if filesystem is configured to support xsendfile.
1889 * @return bool
1891 public function supports_xsendfile() {
1892 return $this->filesystem->supports_xsendfile();
1896 * Content exists
1898 * @param string $contenthash
1899 * @return bool
1900 * @deprecated since 3.3
1902 public function content_exists($contenthash) {
1903 debugging('The content_exists function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1905 return false;
1909 * Tries to recover missing content of file from trash.
1911 * @param stored_file $file stored_file instance
1912 * @return bool success
1913 * @deprecated since 3.3
1915 public function try_content_recovery($file) {
1916 debugging('The try_content_recovery function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1918 return false;
1922 * When user referring to a moodle file, we build the reference field
1924 * @param array $params
1925 * @return string
1927 public static function pack_reference($params) {
1928 $params = (array)$params;
1929 $reference = array();
1930 $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1931 $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1932 $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT);
1933 $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA);
1934 $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);
1935 $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE);
1936 return base64_encode(serialize($reference));
1940 * Unpack reference field
1942 * @param string $str
1943 * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1944 * @throws file_reference_exception if the $str does not have the expected format
1945 * @return array
1947 public static function unpack_reference($str, $cleanparams = false) {
1948 $decoded = base64_decode($str, true);
1949 if ($decoded === false) {
1950 throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1952 $params = @unserialize($decoded); // hide E_NOTICE
1953 if ($params === false) {
1954 throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1956 if (is_array($params) && $cleanparams) {
1957 $params = array(
1958 'component' => is_null($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT),
1959 'filearea' => is_null($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA),
1960 'itemid' => is_null($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT),
1961 'filename' => is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE),
1962 'filepath' => is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH),
1963 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1966 return $params;
1970 * Search through the server files.
1972 * The query parameter will be used in conjuction with the SQL directive
1973 * LIKE, so include '%' in it if you need to. This search will always ignore
1974 * user files and directories. Note that the search is case insensitive.
1976 * This query can quickly become inefficient so use it sparignly.
1978 * @param string $query The string used with SQL LIKE.
1979 * @param integer $from The offset to start the search at.
1980 * @param integer $limit The maximum number of results.
1981 * @param boolean $count When true this methods returns the number of results availabe,
1982 * disregarding the parameters $from and $limit.
1983 * @return int|array Integer when count, otherwise array of stored_file objects.
1985 public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
1986 global $DB;
1987 $params = array(
1988 'contextlevel' => CONTEXT_USER,
1989 'directory' => '.',
1990 'query' => $query
1993 if ($count) {
1994 $select = 'COUNT(1)';
1995 } else {
1996 $select = self::instance_sql_fields('f', 'r');
1998 $like = $DB->sql_like('f.filename', ':query', false);
2000 $sql = "SELECT $select
2001 FROM {files} f
2002 LEFT JOIN {files_reference} r
2003 ON f.referencefileid = r.id
2004 JOIN {context} c
2005 ON f.contextid = c.id
2006 WHERE c.contextlevel <> :contextlevel
2007 AND f.filename <> :directory
2008 AND " . $like . "";
2010 if ($count) {
2011 return $DB->count_records_sql($sql, $params);
2014 $sql .= " ORDER BY f.filename";
2016 $result = array();
2017 $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2018 foreach ($filerecords as $filerecord) {
2019 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2021 $filerecords->close();
2023 return $result;
2027 * Returns all aliases that refer to some stored_file via the given reference
2029 * All repositories that provide access to a stored_file are expected to use
2030 * {@link self::pack_reference()}. This method can't be used if the given reference
2031 * does not use this format or if you are looking for references to an external file
2032 * (for example it can't be used to search for all aliases that refer to a given
2033 * Dropbox or Box.net file).
2035 * Aliases in user draft areas are excluded from the returned list.
2037 * @param string $reference identification of the referenced file
2038 * @return array of stored_file indexed by its pathnamehash
2040 public function search_references($reference) {
2041 global $DB;
2043 if (is_null($reference)) {
2044 throw new coding_exception('NULL is not a valid reference to an external file');
2047 // Give {@link self::unpack_reference()} a chance to throw exception if the
2048 // reference is not in a valid format.
2049 self::unpack_reference($reference);
2051 $referencehash = sha1($reference);
2053 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2054 FROM {files} f
2055 JOIN {files_reference} r ON f.referencefileid = r.id
2056 JOIN {repository_instances} ri ON r.repositoryid = ri.id
2057 WHERE r.referencehash = ?
2058 AND (f.component <> ? OR f.filearea <> ?)";
2060 $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2061 $files = array();
2062 foreach ($rs as $filerecord) {
2063 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2065 $rs->close();
2067 return $files;
2071 * Returns the number of aliases that refer to some stored_file via the given reference
2073 * All repositories that provide access to a stored_file are expected to use
2074 * {@link self::pack_reference()}. This method can't be used if the given reference
2075 * does not use this format or if you are looking for references to an external file
2076 * (for example it can't be used to count aliases that refer to a given Dropbox or
2077 * Box.net file).
2079 * Aliases in user draft areas are not counted.
2081 * @param string $reference identification of the referenced file
2082 * @return int
2084 public function search_references_count($reference) {
2085 global $DB;
2087 if (is_null($reference)) {
2088 throw new coding_exception('NULL is not a valid reference to an external file');
2091 // Give {@link self::unpack_reference()} a chance to throw exception if the
2092 // reference is not in a valid format.
2093 self::unpack_reference($reference);
2095 $referencehash = sha1($reference);
2097 $sql = "SELECT COUNT(f.id)
2098 FROM {files} f
2099 JOIN {files_reference} r ON f.referencefileid = r.id
2100 JOIN {repository_instances} ri ON r.repositoryid = ri.id
2101 WHERE r.referencehash = ?
2102 AND (f.component <> ? OR f.filearea <> ?)";
2104 return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2108 * Returns all aliases that link to the given stored_file
2110 * Aliases in user draft areas are excluded from the returned list.
2112 * @param stored_file $storedfile
2113 * @return array of stored_file
2115 public function get_references_by_storedfile(stored_file $storedfile) {
2116 global $DB;
2118 $params = array();
2119 $params['contextid'] = $storedfile->get_contextid();
2120 $params['component'] = $storedfile->get_component();
2121 $params['filearea'] = $storedfile->get_filearea();
2122 $params['itemid'] = $storedfile->get_itemid();
2123 $params['filename'] = $storedfile->get_filename();
2124 $params['filepath'] = $storedfile->get_filepath();
2126 return $this->search_references(self::pack_reference($params));
2130 * Returns the number of aliases that link to the given stored_file
2132 * Aliases in user draft areas are not counted.
2134 * @param stored_file $storedfile
2135 * @return int
2137 public function get_references_count_by_storedfile(stored_file $storedfile) {
2138 global $DB;
2140 $params = array();
2141 $params['contextid'] = $storedfile->get_contextid();
2142 $params['component'] = $storedfile->get_component();
2143 $params['filearea'] = $storedfile->get_filearea();
2144 $params['itemid'] = $storedfile->get_itemid();
2145 $params['filename'] = $storedfile->get_filename();
2146 $params['filepath'] = $storedfile->get_filepath();
2148 return $this->search_references_count(self::pack_reference($params));
2152 * Updates all files that are referencing this file with the new contenthash
2153 * and filesize
2155 * @param stored_file $storedfile
2157 public function update_references_to_storedfile(stored_file $storedfile) {
2158 global $CFG, $DB;
2159 $params = array();
2160 $params['contextid'] = $storedfile->get_contextid();
2161 $params['component'] = $storedfile->get_component();
2162 $params['filearea'] = $storedfile->get_filearea();
2163 $params['itemid'] = $storedfile->get_itemid();
2164 $params['filename'] = $storedfile->get_filename();
2165 $params['filepath'] = $storedfile->get_filepath();
2166 $reference = self::pack_reference($params);
2167 $referencehash = sha1($reference);
2169 $sql = "SELECT repositoryid, id FROM {files_reference}
2170 WHERE referencehash = ?";
2171 $rs = $DB->get_recordset_sql($sql, array($referencehash));
2173 $now = time();
2174 foreach ($rs as $record) {
2175 $this->update_references($record->id, $now, null,
2176 $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2178 $rs->close();
2182 * Convert file alias to local file
2184 * @throws moodle_exception if file could not be downloaded
2186 * @param stored_file $storedfile a stored_file instances
2187 * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2188 * @return stored_file stored_file
2190 public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2191 global $CFG;
2192 $storedfile->import_external_file_contents($maxbytes);
2193 $storedfile->delete_reference();
2194 return $storedfile;
2198 * Return mimetype by given file pathname.
2200 * If file has a known extension, we return the mimetype based on extension.
2201 * Otherwise (when possible) we try to get the mimetype from file contents.
2203 * @param string $fullpath Full path to the file on disk
2204 * @param string $filename Correct file name with extension, if omitted will be taken from $path
2205 * @return string
2207 public static function mimetype($fullpath, $filename = null) {
2208 if (empty($filename)) {
2209 $filename = $fullpath;
2212 // The mimeinfo function determines the mimetype purely based on the file extension.
2213 $type = mimeinfo('type', $filename);
2215 if ($type === 'document/unknown') {
2216 // The type is unknown. Inspect the file now.
2217 $type = self::mimetype_from_file($fullpath);
2219 return $type;
2223 * Inspect a file on disk for it's mimetype.
2225 * @param string $fullpath Path to file on disk
2226 * @return string The mimetype
2228 public static function mimetype_from_file($fullpath) {
2229 if (file_exists($fullpath)) {
2230 // The type is unknown. Attempt to look up the file type now.
2231 $finfo = new finfo(FILEINFO_MIME_TYPE);
2232 return mimeinfo_from_type('type', $finfo->file($fullpath));
2235 return 'document/unknown';
2239 * Cron cleanup job.
2241 public function cron() {
2242 global $CFG, $DB;
2243 require_once($CFG->libdir.'/cronlib.php');
2245 // find out all stale draft areas (older than 4 days) and purge them
2246 // those are identified by time stamp of the /. root dir
2247 mtrace('Deleting old draft files... ', '');
2248 cron_trace_time_and_memory();
2249 $old = time() - 60*60*24*4;
2250 $sql = "SELECT *
2251 FROM {files}
2252 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2253 AND timecreated < :old";
2254 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2255 foreach ($rs as $dir) {
2256 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2258 $rs->close();
2259 mtrace('done.');
2261 // Remove orphaned files:
2262 // * preview files in the core preview filearea without the existing original file.
2263 // * document converted files in core documentconversion filearea without the existing original file.
2264 mtrace('Deleting orphaned preview, and document conversion files... ', '');
2265 cron_trace_time_and_memory();
2266 $sql = "SELECT p.*
2267 FROM {files} p
2268 LEFT JOIN {files} o ON (p.filename = o.contenthash)
2269 WHERE p.contextid = ?
2270 AND p.component = 'core'
2271 AND (p.filearea = 'preview' OR p.filearea = 'documentconversion')
2272 AND p.itemid = 0
2273 AND o.id IS NULL";
2274 $syscontext = context_system::instance();
2275 $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2276 foreach ($rs as $orphan) {
2277 $file = $this->get_file_instance($orphan);
2278 if (!$file->is_directory()) {
2279 $file->delete();
2282 $rs->close();
2283 mtrace('done.');
2285 // remove trash pool files once a day
2286 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2287 $filescleanupperiod = empty($CFG->filescleanupperiod) ? 86400 : $CFG->filescleanupperiod;
2288 if (empty($CFG->fileslastcleanup) || ($CFG->fileslastcleanup < time() - $filescleanupperiod)) {
2289 require_once($CFG->libdir.'/filelib.php');
2290 // Delete files that are associated with a context that no longer exists.
2291 mtrace('Cleaning up files from deleted contexts... ', '');
2292 cron_trace_time_and_memory();
2293 $sql = "SELECT DISTINCT f.contextid
2294 FROM {files} f
2295 LEFT OUTER JOIN {context} c ON f.contextid = c.id
2296 WHERE c.id IS NULL";
2297 $rs = $DB->get_recordset_sql($sql);
2298 if ($rs->valid()) {
2299 $fs = get_file_storage();
2300 foreach ($rs as $ctx) {
2301 $fs->delete_area_files($ctx->contextid);
2304 $rs->close();
2305 mtrace('done.');
2307 mtrace('Call filesystem cron tasks.', '');
2308 cron_trace_time_and_memory();
2309 $this->filesystem->cron();
2310 mtrace('done.');
2315 * Get the sql formated fields for a file instance to be created from a
2316 * {files} and {files_refernece} join.
2318 * @param string $filesprefix the table prefix for the {files} table
2319 * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2320 * @return string the sql to go after a SELECT
2322 private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2323 // Note, these fieldnames MUST NOT overlap between the two tables,
2324 // else problems like MDL-33172 occur.
2325 $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2326 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2327 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2329 $referencefields = array('repositoryid' => 'repositoryid',
2330 'reference' => 'reference',
2331 'lastsync' => 'referencelastsync');
2333 // id is specifically named to prevent overlaping between the two tables.
2334 $fields = array();
2335 $fields[] = $filesprefix.'.id AS id';
2336 foreach ($filefields as $field) {
2337 $fields[] = "{$filesprefix}.{$field}";
2340 foreach ($referencefields as $field => $alias) {
2341 $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2344 return implode(', ', $fields);
2348 * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2350 * If the record already exists, its id is returned. If there is no such record yet,
2351 * new one is created (using the lastsync provided, too) and its id is returned.
2353 * @param int $repositoryid
2354 * @param string $reference
2355 * @param int $lastsync
2356 * @param int $lifetime argument not used any more
2357 * @return int
2359 private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2360 global $DB;
2362 $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2364 if ($id !== false) {
2365 // bah, that was easy
2366 return $id;
2369 // no such record yet, create one
2370 try {
2371 $id = $DB->insert_record('files_reference', array(
2372 'repositoryid' => $repositoryid,
2373 'reference' => $reference,
2374 'referencehash' => sha1($reference),
2375 'lastsync' => $lastsync));
2376 } catch (dml_exception $e) {
2377 // if inserting the new record failed, chances are that the race condition has just
2378 // occured and the unique index did not allow to create the second record with the same
2379 // repositoryid + reference combo
2380 $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2383 return $id;
2387 * Returns the id of the record in {files_reference} that matches the passed parameters
2389 * Depending on the required strictness, false can be returned. The behaviour is consistent
2390 * with standard DML methods.
2392 * @param int $repositoryid
2393 * @param string $reference
2394 * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2395 * @return int|bool
2397 private function get_referencefileid($repositoryid, $reference, $strictness) {
2398 global $DB;
2400 return $DB->get_field('files_reference', 'id',
2401 array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2405 * Updates a reference to the external resource and all files that use it
2407 * This function is called after synchronisation of an external file and updates the
2408 * contenthash, filesize and status of all files that reference this external file
2409 * as well as time last synchronised.
2411 * @param int $referencefileid
2412 * @param int $lastsync
2413 * @param int $lifetime argument not used any more, liefetime is returned by repository
2414 * @param string $contenthash
2415 * @param int $filesize
2416 * @param int $status 0 if ok or 666 if source is missing
2417 * @param int $timemodified last time modified of the source, if known
2419 public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2420 global $DB;
2421 $referencefileid = clean_param($referencefileid, PARAM_INT);
2422 $lastsync = clean_param($lastsync, PARAM_INT);
2423 validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2424 $filesize = clean_param($filesize, PARAM_INT);
2425 $status = clean_param($status, PARAM_INT);
2426 $params = array('contenthash' => $contenthash,
2427 'filesize' => $filesize,
2428 'status' => $status,
2429 'referencefileid' => $referencefileid,
2430 'timemodified' => $timemodified);
2431 $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2432 status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2433 WHERE referencefileid = :referencefileid', $params);
2434 $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2435 $DB->update_record('files_reference', (object)$data);
2439 * Calculate and return the contenthash of the supplied file.
2441 * @param string $filepath The path to the file on disk
2442 * @return string The file's content hash
2444 public static function hash_from_path($filepath) {
2445 return sha1_file($filepath);
2449 * Calculate and return the contenthash of the supplied content.
2451 * @param string $content The file content
2452 * @return string The file's content hash
2454 public static function hash_from_string($content) {
2455 return sha1($content);