2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
19 * Core file storage class definition.
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");
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.
41 * @copyright 2008 Petr Skoda {@link http://skodak.org}
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 /** @var string Directory with file contents */
48 /** @var string Contents of deleted files not needed any more */
50 /** @var string tempdir */
52 /** @var int Permissions for new directories */
53 private $dirpermissions;
54 /** @var int Permissions for new files */
55 private $filepermissions;
58 * Constructor - do not use directly use {@link get_file_storage()} call instead.
60 * @param string $filedir full path to pool directory
61 * @param string $trashdir temporary storage of deleted area
62 * @param string $tempdir temporary storage of various files
63 * @param int $dirpermissions new directory permissions
64 * @param int $filepermissions new file permissions
66 public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
69 $this->filedir = $filedir;
70 $this->trashdir = $trashdir;
71 $this->tempdir = $tempdir;
72 $this->dirpermissions = $dirpermissions;
73 $this->filepermissions = $filepermissions;
75 // make sure the file pool directory exists
76 if (!is_dir($this->filedir)) {
77 if (!mkdir($this->filedir, $this->dirpermissions, true)) {
78 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
80 // place warning file in file pool root
81 if (!file_exists($this->filedir.'/warning.txt')) {
82 file_put_contents($this->filedir.'/warning.txt',
83 'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
84 chmod($this->filedir.'/warning.txt', $CFG->filepermissions);
87 // make sure the file pool directory exists
88 if (!is_dir($this->trashdir)) {
89 if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
90 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
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
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 === '') {
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
142 public function file_exists_by_hash($pathnamehash) {
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, $this->filedir);
160 * Returns an image file that represent the given stored file as a preview
162 * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
163 * future, the support for other mimetypes can be added, too (eg. generate an image
164 * preview of PDF, text documents etc).
166 * @param stored_file $file the file we want to preview
167 * @param string $mode preview mode, eg. 'thumb'
168 * @return stored_file|bool false if unable to create the preview, stored file otherwise
170 public function get_file_preview(stored_file $file, $mode) {
172 $context = context_system::instance();
173 $path = '/' . trim($mode, '/') . '/';
174 $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
177 $preview = $this->create_file_preview($file, $mode);
187 * Return an available file name.
189 * This will return the next available file name in the area, adding/incrementing a suffix
190 * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
192 * If the file name passed is available without modification, it is returned as is.
194 * @param int $contextid context ID.
195 * @param string $component component.
196 * @param string $filearea file area.
197 * @param int $itemid area item ID.
198 * @param string $filepath the file path.
199 * @param string $filename the file name.
200 * @return string available file name.
201 * @throws coding_exception if the file name is invalid.
204 public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
207 // Do not accept '.' or an empty file name (zero is acceptable).
208 if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
209 throw new coding_exception('Invalid file name passed', $filename);
212 // The file does not exist, we return the same file name.
213 if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
217 // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
218 $pathinfo = pathinfo($filename);
219 $basename = $pathinfo['filename'];
221 if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
222 $basename = $matches[1];
225 $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
226 if (isset($pathinfo['extension'])) {
227 $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
230 $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
231 $filenamelen = $DB->sql_length('f.filename');
232 $sql = "SELECT filename
235 f.contextid = :contextid AND
236 f.component = :component AND
237 f.filearea = :filearea AND
238 f.itemid = :itemid AND
239 f.filepath = :filepath AND
244 $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
245 'filepath' => $filepath, 'filenamelike' => $filenamelike);
246 $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
248 // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
249 // would both be returned, but only the one only containing digits should be used.
251 foreach ($results as $result) {
252 $resultbasename = pathinfo($result, PATHINFO_FILENAME);
254 if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
255 $number = $matches[2] + 1;
260 // Constructing the new filename.
261 $newfilename = $basename . ' (' . $number . ')';
262 if (isset($pathinfo['extension'])) {
263 $newfilename .= '.' . $pathinfo['extension'];
270 * Return an available directory name.
272 * This will return the next available directory name in the area, adding/incrementing a suffix
273 * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
275 * If the file path passed is available without modification, it is returned as is.
277 * @param int $contextid context ID.
278 * @param string $component component.
279 * @param string $filearea file area.
280 * @param int $itemid area item ID.
281 * @param string $suggestedpath the suggested file path.
282 * @return string available file path
285 public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
288 // Ensure suggestedpath has trailing '/'
289 $suggestedpath = rtrim($suggestedpath, '/'). '/';
291 // The directory does not exist, we return the same file path.
292 if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
293 return $suggestedpath;
296 // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
297 if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
298 $suggestedpath = $matches[1]. '/';
301 $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
303 $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
304 $filepathlen = $DB->sql_length('f.filepath');
305 $sql = "SELECT filepath
308 f.contextid = :contextid AND
309 f.component = :component AND
310 f.filearea = :filearea AND
311 f.itemid = :itemid AND
312 f.filename = :filename AND
317 $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
318 'filename' => '.', 'filepathlike' => $filepathlike);
319 $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
321 // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
322 // would both be returned, but only the one only containing digits should be used.
324 foreach ($results as $result) {
325 if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
326 $number = (int)($matches[1]) + 1;
331 return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
335 * Generates a preview image for the stored file
337 * @param stored_file $file the file we want to preview
338 * @param string $mode preview mode, eg. 'thumb'
339 * @return stored_file|bool the newly created preview file or false
341 protected function create_file_preview(stored_file $file, $mode) {
343 $mimetype = $file->get_mimetype();
345 if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
346 // make a preview of the image
347 $data = $this->create_imagefile_preview($file, $mode);
350 // unable to create the preview of this mimetype yet
358 $context = context_system::instance();
360 'contextid' => $context->id,
361 'component' => 'core',
362 'filearea' => 'preview',
364 'filepath' => '/' . trim($mode, '/') . '/',
365 'filename' => $file->get_contenthash(),
368 $imageinfo = getimagesizefromstring($data);
370 $record['mimetype'] = $imageinfo['mime'];
373 return $this->create_file_from_string($record, $data);
377 * Generates a preview for the stored image file
379 * @param stored_file $file the image we want to preview
380 * @param string $mode preview mode, eg. 'thumb'
381 * @return string|bool false if a problem occurs, the thumbnail image data otherwise
383 protected function create_imagefile_preview(stored_file $file, $mode) {
385 require_once($CFG->libdir.'/gdlib.php');
387 $tmproot = make_temp_directory('thumbnails');
388 $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
389 $file->copy_content_to($tmpfilepath);
391 if ($mode === 'tinyicon') {
392 $data = generate_image_thumbnail($tmpfilepath, 24, 24);
394 } else if ($mode === 'thumb') {
395 $data = generate_image_thumbnail($tmpfilepath, 90, 90);
397 } else if ($mode === 'bigthumb') {
398 $data = generate_image_thumbnail($tmpfilepath, 250, 250);
401 throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
404 unlink($tmpfilepath);
410 * Fetch file using local file id.
412 * Please do not rely on file ids, it is usually easier to use
413 * pathname hashes instead.
415 * @param int $fileid file ID
416 * @return stored_file|bool stored_file instance if exists, false if not
418 public function get_file_by_id($fileid) {
421 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
423 LEFT JOIN {files_reference} r
424 ON f.referencefileid = r.id
426 if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
427 return $this->get_file_instance($filerecord);
434 * Fetch file using local file full pathname hash
436 * @param string $pathnamehash path name hash
437 * @return stored_file|bool stored_file instance if exists, false if not
439 public function get_file_by_hash($pathnamehash) {
442 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
444 LEFT JOIN {files_reference} r
445 ON f.referencefileid = r.id
446 WHERE f.pathnamehash = ?";
447 if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
448 return $this->get_file_instance($filerecord);
455 * Fetch locally stored file.
457 * @param int $contextid context ID
458 * @param string $component component
459 * @param string $filearea file area
460 * @param int $itemid item ID
461 * @param string $filepath file path
462 * @param string $filename file name
463 * @return stored_file|bool stored_file instance if exists, false if not
465 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
466 $filepath = clean_param($filepath, PARAM_PATH);
467 $filename = clean_param($filename, PARAM_FILE);
469 if ($filename === '') {
473 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
474 return $this->get_file_by_hash($pathnamehash);
478 * Are there any files (or directories)
480 * @param int $contextid context ID
481 * @param string $component component
482 * @param string $filearea file area
483 * @param bool|int $itemid item id or false if all items
484 * @param bool $ignoredirs whether or not ignore directories
487 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
490 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
491 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
493 if ($itemid !== false) {
494 $params['itemid'] = $itemid;
495 $where .= " AND itemid = :itemid";
501 WHERE $where AND filename <> '.'";
505 WHERE $where AND (filename <> '.' OR filepath <> '/')";
508 return !$DB->record_exists_sql($sql, $params);
512 * Returns all files belonging to given repository
514 * @param int $repositoryid
515 * @param string $sort A fragment of SQL to use for sorting
517 public function get_external_files($repositoryid, $sort = '') {
519 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
521 LEFT JOIN {files_reference} r
522 ON f.referencefileid = r.id
523 WHERE r.repositoryid = ?";
525 $sql .= " ORDER BY {$sort}";
529 $filerecords = $DB->get_records_sql($sql, array($repositoryid));
530 foreach ($filerecords as $filerecord) {
531 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
537 * Returns all area files (optionally limited by itemid)
539 * @param int $contextid context ID
540 * @param string $component component
541 * @param string $filearea file area
542 * @param int $itemid item ID or all files if not specified
543 * @param string $sort A fragment of SQL to use for sorting
544 * @param bool $includedirs whether or not include directories
545 * @return stored_file[] array of stored_files indexed by pathanmehash
547 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
550 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
551 if ($itemid !== false) {
552 $itemidsql = ' AND f.itemid = :itemid ';
553 $conditions['itemid'] = $itemid;
558 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
560 LEFT JOIN {files_reference} r
561 ON f.referencefileid = r.id
562 WHERE f.contextid = :contextid
563 AND f.component = :component
564 AND f.filearea = :filearea
567 $sql .= " ORDER BY {$sort}";
571 $filerecords = $DB->get_records_sql($sql, $conditions);
572 foreach ($filerecords as $filerecord) {
573 if (!$includedirs and $filerecord->filename === '.') {
576 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
582 * Returns array based tree structure of area files
584 * @param int $contextid context ID
585 * @param string $component component
586 * @param string $filearea file area
587 * @param int $itemid item ID
588 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
590 public function get_area_tree($contextid, $component, $filearea, $itemid) {
591 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
592 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
593 // first create directory structure
594 foreach ($files as $hash=>$dir) {
595 if (!$dir->is_directory()) {
598 unset($files[$hash]);
599 if ($dir->get_filepath() === '/') {
600 $result['dirfile'] = $dir;
603 $parts = explode('/', trim($dir->get_filepath(),'/'));
605 foreach ($parts as $part) {
609 if (!isset($pointer['subdirs'][$part])) {
610 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
612 $pointer =& $pointer['subdirs'][$part];
614 $pointer['dirfile'] = $dir;
617 foreach ($files as $hash=>$file) {
618 $parts = explode('/', trim($file->get_filepath(),'/'));
620 foreach ($parts as $part) {
624 $pointer =& $pointer['subdirs'][$part];
626 $pointer['files'][$file->get_filename()] = $file;
629 $result = $this->sort_area_tree($result);
634 * Sorts the result of {@link file_storage::get_area_tree()}.
636 * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
637 * @return array of sorted results
639 protected function sort_area_tree($tree) {
640 foreach ($tree as $key => &$value) {
641 if ($key == 'subdirs') {
642 core_collator::ksort($value, core_collator::SORT_NATURAL);
643 foreach ($value as $subdirname => &$subtree) {
644 $subtree = $this->sort_area_tree($subtree);
646 } else if ($key == 'files') {
647 core_collator::ksort($value, core_collator::SORT_NATURAL);
654 * Returns all files and optionally directories
656 * @param int $contextid context ID
657 * @param string $component component
658 * @param string $filearea file area
659 * @param int $itemid item ID
660 * @param int $filepath directory path
661 * @param bool $recursive include all subdirectories
662 * @param bool $includedirs include files and directories
663 * @param string $sort A fragment of SQL to use for sorting
664 * @return array of stored_files indexed by pathanmehash
666 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
669 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
673 $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
677 $dirs = $includedirs ? "" : "AND filename <> '.'";
678 $length = core_text::strlen($filepath);
680 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
682 LEFT JOIN {files_reference} r
683 ON f.referencefileid = r.id
684 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
685 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
689 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
693 $filerecords = $DB->get_records_sql($sql, $params);
694 foreach ($filerecords as $filerecord) {
695 if ($filerecord->filename == '.') {
696 $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
698 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
701 $result = array_merge($dirs, $files);
705 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
707 $length = core_text::strlen($filepath);
710 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
712 LEFT JOIN {files_reference} r
713 ON f.referencefileid = r.id
714 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
715 AND f.itemid = :itemid AND f.filename = '.'
716 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
719 $reqlevel = substr_count($filepath, '/') + 1;
720 $filerecords = $DB->get_records_sql($sql, $params);
721 foreach ($filerecords as $filerecord) {
722 if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
725 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
729 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
731 LEFT JOIN {files_reference} r
732 ON f.referencefileid = r.id
733 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
734 AND f.filepath = :filepath AND f.filename <> '.'
737 $filerecords = $DB->get_records_sql($sql, $params);
738 foreach ($filerecords as $filerecord) {
739 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
747 * Delete all area files (optionally limited by itemid).
749 * @param int $contextid context ID
750 * @param string $component component
751 * @param string $filearea file area or all areas in context if not specified
752 * @param int $itemid item ID or all files if not specified
753 * @return bool success
755 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
758 $conditions = array('contextid'=>$contextid);
759 if ($component !== false) {
760 $conditions['component'] = $component;
762 if ($filearea !== false) {
763 $conditions['filearea'] = $filearea;
765 if ($itemid !== false) {
766 $conditions['itemid'] = $itemid;
769 $filerecords = $DB->get_records('files', $conditions);
770 foreach ($filerecords as $filerecord) {
771 $this->get_file_instance($filerecord)->delete();
774 return true; // BC only
778 * Delete all the files from certain areas where itemid is limited by an
779 * arbitrary bit of SQL.
781 * @param int $contextid the id of the context the files belong to. Must be given.
782 * @param string $component the owning component. Must be given.
783 * @param string $filearea the file area name. Must be given.
784 * @param string $itemidstest an SQL fragment that the itemid must match. Used
785 * in the query like WHERE itemid $itemidstest. Must used named parameters,
786 * and may not used named parameters called contextid, component or filearea.
787 * @param array $params any query params used by $itemidstest.
789 public function delete_area_files_select($contextid, $component,
790 $filearea, $itemidstest, array $params = null) {
793 $where = "contextid = :contextid
794 AND component = :component
795 AND filearea = :filearea
796 AND itemid $itemidstest";
797 $params['contextid'] = $contextid;
798 $params['component'] = $component;
799 $params['filearea'] = $filearea;
801 $filerecords = $DB->get_recordset_select('files', $where, $params);
802 foreach ($filerecords as $filerecord) {
803 $this->get_file_instance($filerecord)->delete();
805 $filerecords->close();
809 * Delete all files associated with the given component.
811 * @param string $component the component owning the file
813 public function delete_component_files($component) {
816 $filerecords = $DB->get_recordset('files', array('component' => $component));
817 foreach ($filerecords as $filerecord) {
818 $this->get_file_instance($filerecord)->delete();
820 $filerecords->close();
824 * Move all the files in a file area from one context to another.
826 * @param int $oldcontextid the context the files are being moved from.
827 * @param int $newcontextid the context the files are being moved to.
828 * @param string $component the plugin that these files belong to.
829 * @param string $filearea the name of the file area.
830 * @param int $itemid file item ID
831 * @return int the number of files moved, for information.
833 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
834 // Note, this code is based on some code that Petr wrote in
835 // forum_move_attachments in mod/forum/lib.php. I moved it here because
836 // I needed it in the question code too.
839 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
840 foreach ($oldfiles as $oldfile) {
841 $filerecord = new stdClass();
842 $filerecord->contextid = $newcontextid;
843 $this->create_file_from_storedfile($filerecord, $oldfile);
848 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
855 * Recursively creates directory.
857 * @param int $contextid context ID
858 * @param string $component component
859 * @param string $filearea file area
860 * @param int $itemid item ID
861 * @param string $filepath file path
862 * @param int $userid the user ID
863 * @return bool success
865 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
868 // validate all parameters, we do not want any rubbish stored in database, right?
869 if (!is_number($contextid) or $contextid < 1) {
870 throw new file_exception('storedfileproblem', 'Invalid contextid');
873 $component = clean_param($component, PARAM_COMPONENT);
874 if (empty($component)) {
875 throw new file_exception('storedfileproblem', 'Invalid component');
878 $filearea = clean_param($filearea, PARAM_AREA);
879 if (empty($filearea)) {
880 throw new file_exception('storedfileproblem', 'Invalid filearea');
883 if (!is_number($itemid) or $itemid < 0) {
884 throw new file_exception('storedfileproblem', 'Invalid itemid');
887 $filepath = clean_param($filepath, PARAM_PATH);
888 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
889 // path must start and end with '/'
890 throw new file_exception('storedfileproblem', 'Invalid file path');
893 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
895 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
899 static $contenthash = null;
901 $this->add_string_to_pool('');
902 $contenthash = sha1('');
907 $dir_record = new stdClass();
908 $dir_record->contextid = $contextid;
909 $dir_record->component = $component;
910 $dir_record->filearea = $filearea;
911 $dir_record->itemid = $itemid;
912 $dir_record->filepath = $filepath;
913 $dir_record->filename = '.';
914 $dir_record->contenthash = $contenthash;
915 $dir_record->filesize = 0;
917 $dir_record->timecreated = $now;
918 $dir_record->timemodified = $now;
919 $dir_record->mimetype = null;
920 $dir_record->userid = $userid;
922 $dir_record->pathnamehash = $pathnamehash;
924 $DB->insert_record('files', $dir_record);
925 $dir_info = $this->get_file_by_hash($pathnamehash);
927 if ($filepath !== '/') {
928 //recurse to parent dirs
929 $filepath = trim($filepath, '/');
930 $filepath = explode('/', $filepath);
931 array_pop($filepath);
932 $filepath = implode('/', $filepath);
933 $filepath = ($filepath === '') ? '/' : "/$filepath/";
934 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
941 * Add new local file based on existing local file.
943 * @param stdClass|array $filerecord object or array describing changes
944 * @param stored_file|int $fileorid id or stored_file instance of the existing local file
945 * @return stored_file instance of newly created file
947 public function create_file_from_storedfile($filerecord, $fileorid) {
950 if ($fileorid instanceof stored_file) {
951 $fid = $fileorid->get_id();
956 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
958 unset($filerecord['id']);
959 unset($filerecord['filesize']);
960 unset($filerecord['contenthash']);
961 unset($filerecord['pathnamehash']);
963 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
965 LEFT JOIN {files_reference} r
966 ON f.referencefileid = r.id
969 if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
970 throw new file_exception('storedfileproblem', 'File does not exist');
973 unset($newrecord->id);
975 foreach ($filerecord as $key => $value) {
976 // validate all parameters, we do not want any rubbish stored in database, right?
977 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
978 throw new file_exception('storedfileproblem', 'Invalid contextid');
981 if ($key == 'component') {
982 $value = clean_param($value, PARAM_COMPONENT);
984 throw new file_exception('storedfileproblem', 'Invalid component');
988 if ($key == 'filearea') {
989 $value = clean_param($value, PARAM_AREA);
991 throw new file_exception('storedfileproblem', 'Invalid filearea');
995 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
996 throw new file_exception('storedfileproblem', 'Invalid itemid');
1000 if ($key == 'filepath') {
1001 $value = clean_param($value, PARAM_PATH);
1002 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1003 // path must start and end with '/'
1004 throw new file_exception('storedfileproblem', 'Invalid file path');
1008 if ($key == 'filename') {
1009 $value = clean_param($value, PARAM_FILE);
1010 if ($value === '') {
1011 // path must start and end with '/'
1012 throw new file_exception('storedfileproblem', 'Invalid file name');
1016 if ($key === 'timecreated' or $key === 'timemodified') {
1017 if (!is_number($value)) {
1018 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1021 //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)
1026 if ($key == 'referencefileid' or $key == 'referencelastsync') {
1027 $value = clean_param($value, PARAM_INT);
1030 $newrecord->$key = $value;
1033 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1035 if ($newrecord->filename === '.') {
1036 // special case - only this function supports directories ;-)
1037 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1038 // update the existing directory with the new data
1039 $newrecord->id = $directory->get_id();
1040 $DB->update_record('files', $newrecord);
1041 return $this->get_file_instance($newrecord);
1044 // note: referencefileid is copied from the original file so that
1045 // creating a new file from an existing alias creates new alias implicitly.
1046 // here we just check the database consistency.
1047 if (!empty($newrecord->repositoryid)) {
1048 if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1049 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1054 $newrecord->id = $DB->insert_record('files', $newrecord);
1055 } catch (dml_exception $e) {
1056 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1057 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1061 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1063 return $this->get_file_instance($newrecord);
1067 * Add new local file.
1069 * @param stdClass|array $filerecord object or array describing file
1070 * @param string $url the URL to the file
1071 * @param array $options {@link download_file_content()} options
1072 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1073 * @return stored_file
1075 public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1077 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1078 $filerecord = (object)$filerecord; // We support arrays too.
1080 $headers = isset($options['headers']) ? $options['headers'] : null;
1081 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
1082 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
1083 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
1084 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1085 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1086 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
1088 if (!isset($filerecord->filename)) {
1089 $parts = explode('/', $url);
1090 $filename = array_pop($parts);
1091 $filerecord->filename = clean_param($filename, PARAM_FILE);
1093 $source = !empty($filerecord->source) ? $filerecord->source : $url;
1094 $filerecord->source = clean_param($source, PARAM_URL);
1097 check_dir_exists($this->tempdir);
1098 $tmpfile = tempnam($this->tempdir, 'newfromurl');
1099 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1100 if ($content === false) {
1101 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1104 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1107 } catch (Exception $e) {
1113 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1114 if ($content === false) {
1115 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1117 return $this->create_file_from_string($filerecord, $content);
1122 * Add new local file.
1124 * @param stdClass|array $filerecord object or array describing file
1125 * @param string $pathname path to file or content of file
1126 * @return stored_file
1128 public function create_file_from_pathname($filerecord, $pathname) {
1131 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1132 $filerecord = (object)$filerecord; // We support arrays too.
1134 // validate all parameters, we do not want any rubbish stored in database, right?
1135 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1136 throw new file_exception('storedfileproblem', 'Invalid contextid');
1139 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1140 if (empty($filerecord->component)) {
1141 throw new file_exception('storedfileproblem', 'Invalid component');
1144 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1145 if (empty($filerecord->filearea)) {
1146 throw new file_exception('storedfileproblem', 'Invalid filearea');
1149 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1150 throw new file_exception('storedfileproblem', 'Invalid itemid');
1153 if (!empty($filerecord->sortorder)) {
1154 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1155 $filerecord->sortorder = 0;
1158 $filerecord->sortorder = 0;
1161 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1162 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1163 // path must start and end with '/'
1164 throw new file_exception('storedfileproblem', 'Invalid file path');
1167 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1168 if ($filerecord->filename === '') {
1169 // filename must not be empty
1170 throw new file_exception('storedfileproblem', 'Invalid file name');
1174 if (isset($filerecord->timecreated)) {
1175 if (!is_number($filerecord->timecreated)) {
1176 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1178 if ($filerecord->timecreated < 0) {
1179 //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)
1180 $filerecord->timecreated = 0;
1183 $filerecord->timecreated = $now;
1186 if (isset($filerecord->timemodified)) {
1187 if (!is_number($filerecord->timemodified)) {
1188 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1190 if ($filerecord->timemodified < 0) {
1191 //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)
1192 $filerecord->timemodified = 0;
1195 $filerecord->timemodified = $now;
1198 $newrecord = new stdClass();
1200 $newrecord->contextid = $filerecord->contextid;
1201 $newrecord->component = $filerecord->component;
1202 $newrecord->filearea = $filerecord->filearea;
1203 $newrecord->itemid = $filerecord->itemid;
1204 $newrecord->filepath = $filerecord->filepath;
1205 $newrecord->filename = $filerecord->filename;
1207 $newrecord->timecreated = $filerecord->timecreated;
1208 $newrecord->timemodified = $filerecord->timemodified;
1209 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1210 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1211 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1212 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1213 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1214 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1215 $newrecord->sortorder = $filerecord->sortorder;
1217 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1219 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1222 $newrecord->id = $DB->insert_record('files', $newrecord);
1223 } catch (dml_exception $e) {
1225 $this->deleted_file_cleanup($newrecord->contenthash);
1227 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1228 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1231 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1233 return $this->get_file_instance($newrecord);
1237 * Add new local file.
1239 * @param stdClass|array $filerecord object or array describing file
1240 * @param string $content content of file
1241 * @return stored_file
1243 public function create_file_from_string($filerecord, $content) {
1246 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1247 $filerecord = (object)$filerecord; // We support arrays too.
1249 // validate all parameters, we do not want any rubbish stored in database, right?
1250 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1251 throw new file_exception('storedfileproblem', 'Invalid contextid');
1254 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1255 if (empty($filerecord->component)) {
1256 throw new file_exception('storedfileproblem', 'Invalid component');
1259 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1260 if (empty($filerecord->filearea)) {
1261 throw new file_exception('storedfileproblem', 'Invalid filearea');
1264 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1265 throw new file_exception('storedfileproblem', 'Invalid itemid');
1268 if (!empty($filerecord->sortorder)) {
1269 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1270 $filerecord->sortorder = 0;
1273 $filerecord->sortorder = 0;
1276 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1277 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1278 // path must start and end with '/'
1279 throw new file_exception('storedfileproblem', 'Invalid file path');
1282 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1283 if ($filerecord->filename === '') {
1284 // path must start and end with '/'
1285 throw new file_exception('storedfileproblem', 'Invalid file name');
1289 if (isset($filerecord->timecreated)) {
1290 if (!is_number($filerecord->timecreated)) {
1291 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1293 if ($filerecord->timecreated < 0) {
1294 //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)
1295 $filerecord->timecreated = 0;
1298 $filerecord->timecreated = $now;
1301 if (isset($filerecord->timemodified)) {
1302 if (!is_number($filerecord->timemodified)) {
1303 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1305 if ($filerecord->timemodified < 0) {
1306 //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)
1307 $filerecord->timemodified = 0;
1310 $filerecord->timemodified = $now;
1313 $newrecord = new stdClass();
1315 $newrecord->contextid = $filerecord->contextid;
1316 $newrecord->component = $filerecord->component;
1317 $newrecord->filearea = $filerecord->filearea;
1318 $newrecord->itemid = $filerecord->itemid;
1319 $newrecord->filepath = $filerecord->filepath;
1320 $newrecord->filename = $filerecord->filename;
1322 $newrecord->timecreated = $filerecord->timecreated;
1323 $newrecord->timemodified = $filerecord->timemodified;
1324 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1325 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1326 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1327 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1328 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1329 $newrecord->sortorder = $filerecord->sortorder;
1331 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1332 $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1333 // get mimetype by magic bytes
1334 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1336 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1339 $newrecord->id = $DB->insert_record('files', $newrecord);
1340 } catch (dml_exception $e) {
1342 $this->deleted_file_cleanup($newrecord->contenthash);
1344 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1345 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1348 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1350 return $this->get_file_instance($newrecord);
1354 * Create a new alias/shortcut file from file reference information
1356 * @param stdClass|array $filerecord object or array describing the new file
1357 * @param int $repositoryid the id of the repository that provides the original file
1358 * @param string $reference the information required by the repository to locate the original file
1359 * @param array $options options for creating the new file
1360 * @return stored_file
1362 public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1365 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1366 $filerecord = (object)$filerecord; // We support arrays too.
1368 // validate all parameters, we do not want any rubbish stored in database, right?
1369 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1370 throw new file_exception('storedfileproblem', 'Invalid contextid');
1373 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1374 if (empty($filerecord->component)) {
1375 throw new file_exception('storedfileproblem', 'Invalid component');
1378 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1379 if (empty($filerecord->filearea)) {
1380 throw new file_exception('storedfileproblem', 'Invalid filearea');
1383 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1384 throw new file_exception('storedfileproblem', 'Invalid itemid');
1387 if (!empty($filerecord->sortorder)) {
1388 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1389 $filerecord->sortorder = 0;
1392 $filerecord->sortorder = 0;
1395 $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1396 $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1397 $filerecord->source = empty($filerecord->source) ? null : $filerecord->source;
1398 $filerecord->author = empty($filerecord->author) ? null : $filerecord->author;
1399 $filerecord->license = empty($filerecord->license) ? null : $filerecord->license;
1400 $filerecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1401 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1402 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1403 // Path must start and end with '/'.
1404 throw new file_exception('storedfileproblem', 'Invalid file path');
1407 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1408 if ($filerecord->filename === '') {
1409 // Path must start and end with '/'.
1410 throw new file_exception('storedfileproblem', 'Invalid file name');
1414 if (isset($filerecord->timecreated)) {
1415 if (!is_number($filerecord->timecreated)) {
1416 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1418 if ($filerecord->timecreated < 0) {
1419 // 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)
1420 $filerecord->timecreated = 0;
1423 $filerecord->timecreated = $now;
1426 if (isset($filerecord->timemodified)) {
1427 if (!is_number($filerecord->timemodified)) {
1428 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1430 if ($filerecord->timemodified < 0) {
1431 // 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)
1432 $filerecord->timemodified = 0;
1435 $filerecord->timemodified = $now;
1438 $transaction = $DB->start_delegated_transaction();
1441 $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1442 } catch (Exception $e) {
1443 throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1446 if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1447 // there was specified the contenthash for a file already stored in moodle filepool
1448 if (empty($filerecord->filesize)) {
1449 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1450 $filerecord->filesize = filesize($filepathname);
1452 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1455 // atempt to get the result of last synchronisation for this reference
1456 $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1457 'id, contenthash, filesize', IGNORE_MULTIPLE);
1459 $filerecord->contenthash = $lastcontent->contenthash;
1460 $filerecord->filesize = $lastcontent->filesize;
1462 // External file doesn't have content in moodle.
1463 // So we create an empty file for it.
1464 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1468 $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1471 $filerecord->id = $DB->insert_record('files', $filerecord);
1472 } catch (dml_exception $e) {
1473 if (!empty($newfile)) {
1474 $this->deleted_file_cleanup($filerecord->contenthash);
1476 throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1477 $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1480 $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1482 $transaction->allow_commit();
1484 // this will retrieve all reference information from DB as well
1485 return $this->get_file_by_id($filerecord->id);
1489 * Creates new image file from existing.
1491 * @param stdClass|array $filerecord object or array describing new file
1492 * @param int|stored_file $fid file id or stored file object
1493 * @param int $newwidth in pixels
1494 * @param int $newheight in pixels
1495 * @param bool $keepaspectratio whether or not keep aspect ratio
1496 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1497 * @return stored_file
1499 public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1500 if (!function_exists('imagecreatefromstring')) {
1501 //Most likely the GD php extension isn't installed
1502 //image conversion cannot succeed
1503 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1506 if ($fid instanceof stored_file) {
1507 $fid = $fid->get_id();
1510 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1512 if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1513 throw new file_exception('storedfileproblem', 'File does not exist');
1516 if (!$imageinfo = $file->get_imageinfo()) {
1517 throw new file_exception('storedfileproblem', 'File is not an image');
1520 if (!isset($filerecord['filename'])) {
1521 $filerecord['filename'] = $file->get_filename();
1524 if (!isset($filerecord['mimetype'])) {
1525 $filerecord['mimetype'] = $imageinfo['mimetype'];
1528 $width = $imageinfo['width'];
1529 $height = $imageinfo['height'];
1530 $mimetype = $imageinfo['mimetype'];
1532 if ($keepaspectratio) {
1533 if (0 >= $newwidth and 0 >= $newheight) {
1534 // no sizes specified
1536 $newheight = $height;
1538 } else if (0 < $newwidth and 0 < $newheight) {
1539 $xheight = ($newwidth*($height/$width));
1540 if ($xheight < $newheight) {
1541 $newheight = (int)$xheight;
1543 $newwidth = (int)($newheight*($width/$height));
1546 } else if (0 < $newwidth) {
1547 $newheight = (int)($newwidth*($height/$width));
1549 } else { //0 < $newheight
1550 $newwidth = (int)($newheight*($width/$height));
1554 if (0 >= $newwidth) {
1557 if (0 >= $newheight) {
1558 $newheight = $height;
1562 $img = imagecreatefromstring($file->get_content());
1563 if ($height != $newheight or $width != $newwidth) {
1564 $newimg = imagecreatetruecolor($newwidth, $newheight);
1565 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1567 throw new file_exception('storedfileproblem', 'Can not resize image');
1574 switch ($filerecord['mimetype']) {
1580 if (is_null($quality)) {
1583 imagejpeg($img, NULL, $quality);
1588 $quality = (int)$quality;
1589 imagepng($img, NULL, $quality, NULL);
1593 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1596 $content = ob_get_contents();
1601 throw new file_exception('storedfileproblem', 'Can not convert image');
1604 return $this->create_file_from_string($filerecord, $content);
1608 * Add file content to sha1 pool.
1610 * @param string $pathname path to file
1611 * @param string $contenthash sha1 hash of content if known (performance only)
1612 * @return array (contenthash, filesize, newfile)
1614 public function add_file_to_pool($pathname, $contenthash = NULL) {
1617 if (!is_readable($pathname)) {
1618 throw new file_exception('storedfilecannotread', '', $pathname);
1621 $filesize = filesize($pathname);
1622 if ($filesize === false) {
1623 throw new file_exception('storedfilecannotread', '', $pathname);
1626 if (is_null($contenthash)) {
1627 $contenthash = sha1_file($pathname);
1628 } else if ($CFG->debugdeveloper) {
1629 $filehash = sha1_file($pathname);
1630 if ($filehash === false) {
1631 throw new file_exception('storedfilecannotread', '', $pathname);
1633 if ($filehash !== $contenthash) {
1634 // Hopefully this never happens, if yes we need to fix calling code.
1635 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1636 $contenthash = $filehash;
1639 if ($contenthash === false) {
1640 throw new file_exception('storedfilecannotread', '', $pathname);
1643 if ($filesize > 0 and $contenthash === sha1('')) {
1644 // Did the file change or is sha1_file() borked for this file?
1646 $contenthash = sha1_file($pathname);
1647 $filesize = filesize($pathname);
1649 if ($contenthash === false or $filesize === false) {
1650 throw new file_exception('storedfilecannotread', '', $pathname);
1652 if ($filesize > 0 and $contenthash === sha1('')) {
1653 // This is very weird...
1654 throw new file_exception('storedfilecannotread', '', $pathname);
1658 $hashpath = $this->path_from_hash($contenthash);
1659 $hashfile = "$hashpath/$contenthash";
1663 if (file_exists($hashfile)) {
1664 if (filesize($hashfile) === $filesize) {
1665 return array($contenthash, $filesize, false);
1667 if (sha1_file($hashfile) === $contenthash) {
1668 // Jackpot! We have a sha1 collision.
1669 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1670 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1671 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1672 throw new file_pool_content_exception($contenthash);
1674 debugging("Replacing invalid content file $contenthash");
1679 if (!is_dir($hashpath)) {
1680 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1681 // Permission trouble.
1682 throw new file_exception('storedfilecannotcreatefiledirs');
1686 // Let's try to prevent some race conditions.
1688 $prev = ignore_user_abort(true);
1689 @unlink($hashfile.'.tmp');
1690 if (!copy($pathname, $hashfile.'.tmp')) {
1691 // Borked permissions or out of disk space.
1692 ignore_user_abort($prev);
1693 throw new file_exception('storedfilecannotcreatefile');
1695 if (filesize($hashfile.'.tmp') !== $filesize) {
1696 // This should not happen.
1697 unlink($hashfile.'.tmp');
1698 ignore_user_abort($prev);
1699 throw new file_exception('storedfilecannotcreatefile');
1701 rename($hashfile.'.tmp', $hashfile);
1702 chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1703 @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1704 ignore_user_abort($prev);
1706 return array($contenthash, $filesize, $newfile);
1710 * Add string content to sha1 pool.
1712 * @param string $content file content - binary string
1713 * @return array (contenthash, filesize, newfile)
1715 public function add_string_to_pool($content) {
1718 $contenthash = sha1($content);
1719 $filesize = strlen($content); // binary length
1721 $hashpath = $this->path_from_hash($contenthash);
1722 $hashfile = "$hashpath/$contenthash";
1726 if (file_exists($hashfile)) {
1727 if (filesize($hashfile) === $filesize) {
1728 return array($contenthash, $filesize, false);
1730 if (sha1_file($hashfile) === $contenthash) {
1731 // Jackpot! We have a sha1 collision.
1732 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1733 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
1734 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
1735 throw new file_pool_content_exception($contenthash);
1737 debugging("Replacing invalid content file $contenthash");
1742 if (!is_dir($hashpath)) {
1743 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1744 // Permission trouble.
1745 throw new file_exception('storedfilecannotcreatefiledirs');
1749 // Hopefully this works around most potential race conditions.
1751 $prev = ignore_user_abort(true);
1753 if (!empty($CFG->preventfilelocking)) {
1754 $newsize = file_put_contents($hashfile.'.tmp', $content);
1756 $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
1759 if ($newsize === false) {
1760 // Borked permissions most likely.
1761 ignore_user_abort($prev);
1762 throw new file_exception('storedfilecannotcreatefile');
1764 if (filesize($hashfile.'.tmp') !== $filesize) {
1765 // Out of disk space?
1766 unlink($hashfile.'.tmp');
1767 ignore_user_abort($prev);
1768 throw new file_exception('storedfilecannotcreatefile');
1770 rename($hashfile.'.tmp', $hashfile);
1771 chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1772 @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1773 ignore_user_abort($prev);
1775 return array($contenthash, $filesize, $newfile);
1779 * Serve file content using X-Sendfile header.
1780 * Please make sure that all headers are already sent
1781 * and the all access control checks passed.
1783 * @param string $contenthash sah1 hash of the file content to be served
1784 * @return bool success
1786 public function xsendfile($contenthash) {
1788 require_once("$CFG->libdir/xsendfilelib.php");
1790 $hashpath = $this->path_from_hash($contenthash);
1791 return xsendfile("$hashpath/$contenthash");
1797 * @param string $contenthash
1800 public function content_exists($contenthash) {
1801 $dir = $this->path_from_hash($contenthash);
1802 $filepath = $dir . '/' . $contenthash;
1803 return file_exists($filepath);
1807 * Return path to file with given hash.
1809 * NOTE: must not be public, files in pool must not be modified
1811 * @param string $contenthash content hash
1812 * @return string expected file location
1814 protected function path_from_hash($contenthash) {
1815 $l1 = $contenthash[0].$contenthash[1];
1816 $l2 = $contenthash[2].$contenthash[3];
1817 return "$this->filedir/$l1/$l2";
1821 * Return path to file with given hash.
1823 * NOTE: must not be public, files in pool must not be modified
1825 * @param string $contenthash content hash
1826 * @return string expected file location
1828 protected function trash_path_from_hash($contenthash) {
1829 $l1 = $contenthash[0].$contenthash[1];
1830 $l2 = $contenthash[2].$contenthash[3];
1831 return "$this->trashdir/$l1/$l2";
1835 * Tries to recover missing content of file from trash.
1837 * @param stored_file $file stored_file instance
1838 * @return bool success
1840 public function try_content_recovery($file) {
1841 $contenthash = $file->get_contenthash();
1842 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1843 if (!is_readable($trashfile)) {
1844 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1847 // nice, at least alternative trash file in trash root exists
1848 $trashfile = $this->trashdir.'/'.$contenthash;
1850 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1851 //weird, better fail early
1854 $contentdir = $this->path_from_hash($contenthash);
1855 $contentfile = $contentdir.'/'.$contenthash;
1856 if (file_exists($contentfile)) {
1857 //strange, no need to recover anything
1860 if (!is_dir($contentdir)) {
1861 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1865 return rename($trashfile, $contentfile);
1869 * Marks pool file as candidate for deleting.
1871 * DO NOT call directly - reserved for core!!
1873 * @param string $contenthash
1875 public function deleted_file_cleanup($contenthash) {
1878 if ($contenthash === sha1('')) {
1879 // No need to delete empty content file with sha1('') content hash.
1883 //Note: this section is critical - in theory file could be reused at the same
1884 // time, if this happens we can still recover the file from trash
1885 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1886 // file content is still used
1889 //move content file to trash
1890 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1891 if (!file_exists($contentfile)) {
1892 //weird, but no problem
1895 $trashpath = $this->trash_path_from_hash($contenthash);
1896 $trashfile = $trashpath.'/'.$contenthash;
1897 if (file_exists($trashfile)) {
1898 // we already have this content in trash, no need to move it there
1899 unlink($contentfile);
1902 if (!is_dir($trashpath)) {
1903 mkdir($trashpath, $this->dirpermissions, true);
1905 rename($contentfile, $trashfile);
1906 chmod($trashfile, $this->filepermissions); // fix permissions if needed
1910 * When user referring to a moodle file, we build the reference field
1912 * @param array $params
1915 public static function pack_reference($params) {
1916 $params = (array)$params;
1917 $reference = array();
1918 $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1919 $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1920 $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT);
1921 $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA);
1922 $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);
1923 $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE);
1924 return base64_encode(serialize($reference));
1928 * Unpack reference field
1930 * @param string $str
1931 * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1932 * @throws file_reference_exception if the $str does not have the expected format
1935 public static function unpack_reference($str, $cleanparams = false) {
1936 $decoded = base64_decode($str, true);
1937 if ($decoded === false) {
1938 throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1940 $params = @unserialize($decoded); // hide E_NOTICE
1941 if ($params === false) {
1942 throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1944 if (is_array($params) && $cleanparams) {
1946 'component' => is_null($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT),
1947 'filearea' => is_null($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA),
1948 'itemid' => is_null($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT),
1949 'filename' => is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE),
1950 'filepath' => is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH),
1951 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1958 * Search through the server files.
1960 * The query parameter will be used in conjuction with the SQL directive
1961 * LIKE, so include '%' in it if you need to. This search will always ignore
1962 * user files and directories. Note that the search is case insensitive.
1964 * This query can quickly become inefficient so use it sparignly.
1966 * @param string $query The string used with SQL LIKE.
1967 * @param integer $from The offset to start the search at.
1968 * @param integer $limit The maximum number of results.
1969 * @param boolean $count When true this methods returns the number of results availabe,
1970 * disregarding the parameters $from and $limit.
1971 * @return int|array Integer when count, otherwise array of stored_file objects.
1973 public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
1976 'contextlevel' => CONTEXT_USER,
1982 $select = 'COUNT(1)';
1984 $select = self::instance_sql_fields('f', 'r');
1986 $like = $DB->sql_like('f.filename', ':query', false);
1988 $sql = "SELECT $select
1990 LEFT JOIN {files_reference} r
1991 ON f.referencefileid = r.id
1993 ON f.contextid = c.id
1994 WHERE c.contextlevel <> :contextlevel
1995 AND f.filename <> :directory
1999 return $DB->count_records_sql($sql, $params);
2002 $sql .= " ORDER BY f.filename";
2005 $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2006 foreach ($filerecords as $filerecord) {
2007 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2009 $filerecords->close();
2015 * Returns all aliases that refer to some stored_file via the given reference
2017 * All repositories that provide access to a stored_file are expected to use
2018 * {@link self::pack_reference()}. This method can't be used if the given reference
2019 * does not use this format or if you are looking for references to an external file
2020 * (for example it can't be used to search for all aliases that refer to a given
2021 * Dropbox or Box.net file).
2023 * Aliases in user draft areas are excluded from the returned list.
2025 * @param string $reference identification of the referenced file
2026 * @return array of stored_file indexed by its pathnamehash
2028 public function search_references($reference) {
2031 if (is_null($reference)) {
2032 throw new coding_exception('NULL is not a valid reference to an external file');
2035 // Give {@link self::unpack_reference()} a chance to throw exception if the
2036 // reference is not in a valid format.
2037 self::unpack_reference($reference);
2039 $referencehash = sha1($reference);
2041 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2043 JOIN {files_reference} r ON f.referencefileid = r.id
2044 JOIN {repository_instances} ri ON r.repositoryid = ri.id
2045 WHERE r.referencehash = ?
2046 AND (f.component <> ? OR f.filearea <> ?)";
2048 $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2050 foreach ($rs as $filerecord) {
2051 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2058 * Returns the number of aliases that refer to some stored_file via the given reference
2060 * All repositories that provide access to a stored_file are expected to use
2061 * {@link self::pack_reference()}. This method can't be used if the given reference
2062 * does not use this format or if you are looking for references to an external file
2063 * (for example it can't be used to count aliases that refer to a given Dropbox or
2066 * Aliases in user draft areas are not counted.
2068 * @param string $reference identification of the referenced file
2071 public function search_references_count($reference) {
2074 if (is_null($reference)) {
2075 throw new coding_exception('NULL is not a valid reference to an external file');
2078 // Give {@link self::unpack_reference()} a chance to throw exception if the
2079 // reference is not in a valid format.
2080 self::unpack_reference($reference);
2082 $referencehash = sha1($reference);
2084 $sql = "SELECT COUNT(f.id)
2086 JOIN {files_reference} r ON f.referencefileid = r.id
2087 JOIN {repository_instances} ri ON r.repositoryid = ri.id
2088 WHERE r.referencehash = ?
2089 AND (f.component <> ? OR f.filearea <> ?)";
2091 return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2095 * Returns all aliases that link to the given stored_file
2097 * Aliases in user draft areas are excluded from the returned list.
2099 * @param stored_file $storedfile
2100 * @return array of stored_file
2102 public function get_references_by_storedfile(stored_file $storedfile) {
2106 $params['contextid'] = $storedfile->get_contextid();
2107 $params['component'] = $storedfile->get_component();
2108 $params['filearea'] = $storedfile->get_filearea();
2109 $params['itemid'] = $storedfile->get_itemid();
2110 $params['filename'] = $storedfile->get_filename();
2111 $params['filepath'] = $storedfile->get_filepath();
2113 return $this->search_references(self::pack_reference($params));
2117 * Returns the number of aliases that link to the given stored_file
2119 * Aliases in user draft areas are not counted.
2121 * @param stored_file $storedfile
2124 public function get_references_count_by_storedfile(stored_file $storedfile) {
2128 $params['contextid'] = $storedfile->get_contextid();
2129 $params['component'] = $storedfile->get_component();
2130 $params['filearea'] = $storedfile->get_filearea();
2131 $params['itemid'] = $storedfile->get_itemid();
2132 $params['filename'] = $storedfile->get_filename();
2133 $params['filepath'] = $storedfile->get_filepath();
2135 return $this->search_references_count(self::pack_reference($params));
2139 * Updates all files that are referencing this file with the new contenthash
2142 * @param stored_file $storedfile
2144 public function update_references_to_storedfile(stored_file $storedfile) {
2147 $params['contextid'] = $storedfile->get_contextid();
2148 $params['component'] = $storedfile->get_component();
2149 $params['filearea'] = $storedfile->get_filearea();
2150 $params['itemid'] = $storedfile->get_itemid();
2151 $params['filename'] = $storedfile->get_filename();
2152 $params['filepath'] = $storedfile->get_filepath();
2153 $reference = self::pack_reference($params);
2154 $referencehash = sha1($reference);
2156 $sql = "SELECT repositoryid, id FROM {files_reference}
2157 WHERE referencehash = ?";
2158 $rs = $DB->get_recordset_sql($sql, array($referencehash));
2161 foreach ($rs as $record) {
2162 $this->update_references($record->id, $now, null,
2163 $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
2169 * Convert file alias to local file
2171 * @throws moodle_exception if file could not be downloaded
2173 * @param stored_file $storedfile a stored_file instances
2174 * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2175 * @return stored_file stored_file
2177 public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2179 $storedfile->import_external_file_contents($maxbytes);
2180 $storedfile->delete_reference();
2185 * Return mimetype by given file pathname
2187 * If file has a known extension, we return the mimetype based on extension.
2188 * Otherwise (when possible) we try to get the mimetype from file contents.
2190 * @param string $pathname full path to the file
2191 * @param string $filename correct file name with extension, if omitted will be taken from $path
2194 public static function mimetype($pathname, $filename = null) {
2195 if (empty($filename)) {
2196 $filename = $pathname;
2198 $type = mimeinfo('type', $filename);
2199 if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2200 $finfo = new finfo(FILEINFO_MIME_TYPE);
2201 $type = mimeinfo_from_type('type', $finfo->file($pathname));
2209 public function cron() {
2211 require_once($CFG->libdir.'/cronlib.php');
2213 // find out all stale draft areas (older than 4 days) and purge them
2214 // those are identified by time stamp of the /. root dir
2215 mtrace('Deleting old draft files... ', '');
2216 cron_trace_time_and_memory();
2217 $old = time() - 60*60*24*4;
2220 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2221 AND timecreated < :old";
2222 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2223 foreach ($rs as $dir) {
2224 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2229 // remove orphaned preview files (that is files in the core preview filearea without
2230 // the existing original file)
2231 mtrace('Deleting orphaned preview files... ', '');
2232 cron_trace_time_and_memory();
2235 LEFT JOIN {files} o ON (p.filename = o.contenthash)
2236 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2238 $syscontext = context_system::instance();
2239 $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2240 foreach ($rs as $orphan) {
2241 $file = $this->get_file_instance($orphan);
2242 if (!$file->is_directory()) {
2249 // remove trash pool files once a day
2250 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2251 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2252 require_once($CFG->libdir.'/filelib.php');
2253 // Delete files that are associated with a context that no longer exists.
2254 mtrace('Cleaning up files from deleted contexts... ', '');
2255 cron_trace_time_and_memory();
2256 $sql = "SELECT DISTINCT f.contextid
2258 LEFT OUTER JOIN {context} c ON f.contextid = c.id
2259 WHERE c.id IS NULL";
2260 $rs = $DB->get_recordset_sql($sql);
2262 $fs = get_file_storage();
2263 foreach ($rs as $ctx) {
2264 $fs->delete_area_files($ctx->contextid);
2270 mtrace('Deleting trash files... ', '');
2271 cron_trace_time_and_memory();
2272 fulldelete($this->trashdir);
2273 set_config('fileslastcleanup', time());
2279 * Get the sql formated fields for a file instance to be created from a
2280 * {files} and {files_refernece} join.
2282 * @param string $filesprefix the table prefix for the {files} table
2283 * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2284 * @return string the sql to go after a SELECT
2286 private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2287 // Note, these fieldnames MUST NOT overlap between the two tables,
2288 // else problems like MDL-33172 occur.
2289 $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2290 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2291 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2293 $referencefields = array('repositoryid' => 'repositoryid',
2294 'reference' => 'reference',
2295 'lastsync' => 'referencelastsync');
2297 // id is specifically named to prevent overlaping between the two tables.
2299 $fields[] = $filesprefix.'.id AS id';
2300 foreach ($filefields as $field) {
2301 $fields[] = "{$filesprefix}.{$field}";
2304 foreach ($referencefields as $field => $alias) {
2305 $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2308 return implode(', ', $fields);
2312 * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2314 * If the record already exists, its id is returned. If there is no such record yet,
2315 * new one is created (using the lastsync provided, too) and its id is returned.
2317 * @param int $repositoryid
2318 * @param string $reference
2319 * @param int $lastsync
2320 * @param int $lifetime argument not used any more
2323 private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2326 $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2328 if ($id !== false) {
2329 // bah, that was easy
2333 // no such record yet, create one
2335 $id = $DB->insert_record('files_reference', array(
2336 'repositoryid' => $repositoryid,
2337 'reference' => $reference,
2338 'referencehash' => sha1($reference),
2339 'lastsync' => $lastsync));
2340 } catch (dml_exception $e) {
2341 // if inserting the new record failed, chances are that the race condition has just
2342 // occured and the unique index did not allow to create the second record with the same
2343 // repositoryid + reference combo
2344 $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2351 * Returns the id of the record in {files_reference} that matches the passed parameters
2353 * Depending on the required strictness, false can be returned. The behaviour is consistent
2354 * with standard DML methods.
2356 * @param int $repositoryid
2357 * @param string $reference
2358 * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2361 private function get_referencefileid($repositoryid, $reference, $strictness) {
2364 return $DB->get_field('files_reference', 'id',
2365 array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2369 * Updates a reference to the external resource and all files that use it
2371 * This function is called after synchronisation of an external file and updates the
2372 * contenthash, filesize and status of all files that reference this external file
2373 * as well as time last synchronised.
2375 * @param int $referencefileid
2376 * @param int $lastsync
2377 * @param int $lifetime argument not used any more, liefetime is returned by repository
2378 * @param string $contenthash
2379 * @param int $filesize
2380 * @param int $status 0 if ok or 666 if source is missing
2382 public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2384 $referencefileid = clean_param($referencefileid, PARAM_INT);
2385 $lastsync = clean_param($lastsync, PARAM_INT);
2386 validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2387 $filesize = clean_param($filesize, PARAM_INT);
2388 $status = clean_param($status, PARAM_INT);
2389 $params = array('contenthash' => $contenthash,
2390 'filesize' => $filesize,
2391 'status' => $status,
2392 'referencefileid' => $referencefileid);
2393 $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2395 WHERE referencefileid = :referencefileid', $params);
2396 $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2397 $DB->update_record('files_reference', (object)$data);