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) {
67 $this->filedir = $filedir;
68 $this->trashdir = $trashdir;
69 $this->tempdir = $tempdir;
70 $this->dirpermissions = $dirpermissions;
71 $this->filepermissions = $filepermissions;
73 // make sure the file pool directory exists
74 if (!is_dir($this->filedir)) {
75 if (!mkdir($this->filedir, $this->dirpermissions, true)) {
76 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
78 // place warning file in file pool root
79 if (!file_exists($this->filedir.'/warning.txt')) {
80 file_put_contents($this->filedir.'/warning.txt',
81 '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 // make sure the file pool directory exists
85 if (!is_dir($this->trashdir)) {
86 if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
87 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
93 * Calculates sha1 hash of unique full path name information.
95 * This hash is a unique file identifier - it is used to improve
96 * performance and overcome db index size limits.
98 * @param int $contextid context ID
99 * @param string $component component
100 * @param string $filearea file area
101 * @param int $itemid item ID
102 * @param string $filepath file path
103 * @param string $filename file name
104 * @return string sha1 hash
106 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
111 * Does this file exist?
113 * @param int $contextid context ID
114 * @param string $component component
115 * @param string $filearea file area
116 * @param int $itemid item ID
117 * @param string $filepath file path
118 * @param string $filename file name
121 public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
122 $filepath = clean_param($filepath, PARAM_PATH);
123 $filename = clean_param($filename, PARAM_FILE);
125 if ($filename === '') {
129 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
130 return $this->file_exists_by_hash($pathnamehash);
134 * Whether or not the file exist
136 * @param string $pathnamehash path name hash
139 public function file_exists_by_hash($pathnamehash) {
142 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
146 * Create instance of file class from database record.
148 * @param stdClass $filerecord record from the files table left join files_reference table
149 * @return stored_file instance of file abstraction class
151 public function get_file_instance(stdClass $filerecord) {
152 $storedfile = new stored_file($this, $filerecord, $this->filedir);
157 * Returns an image file that represent the given stored file as a preview
159 * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
160 * future, the support for other mimetypes can be added, too (eg. generate an image
161 * preview of PDF, text documents etc).
163 * @param stored_file $file the file we want to preview
164 * @param string $mode preview mode, eg. 'thumb'
165 * @return stored_file|bool false if unable to create the preview, stored file otherwise
167 public function get_file_preview(stored_file $file, $mode) {
169 $context = context_system::instance();
170 $path = '/' . trim($mode, '/') . '/';
171 $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
174 $preview = $this->create_file_preview($file, $mode);
184 * Return an available file name.
186 * This will return the next available file name in the area, adding/incrementing a suffix
187 * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
189 * If the file name passed is available without modification, it is returned as is.
191 * @param int $contextid context ID.
192 * @param string $component component.
193 * @param string $filearea file area.
194 * @param int $itemid area item ID.
195 * @param string $filepath the file path.
196 * @param string $filename the file name.
197 * @return string available file name.
198 * @throws coding_exception if the file name is invalid.
201 public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
204 // Do not accept '.' or an empty file name (zero is acceptable).
205 if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
206 throw new coding_exception('Invalid file name passed', $filename);
209 // The file does not exist, we return the same file name.
210 if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
214 // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
215 $pathinfo = pathinfo($filename);
216 $basename = $pathinfo['filename'];
218 if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
219 $basename = $matches[1];
222 $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
223 if (isset($pathinfo['extension'])) {
224 $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
227 $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
228 $filenamelen = $DB->sql_length('f.filename');
229 $sql = "SELECT filename
232 f.contextid = :contextid AND
233 f.component = :component AND
234 f.filearea = :filearea AND
235 f.itemid = :itemid AND
236 f.filepath = :filepath AND
241 $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
242 'filepath' => $filepath, 'filenamelike' => $filenamelike);
243 $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
245 // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
246 // would both be returned, but only the one only containing digits should be used.
248 foreach ($results as $result) {
249 $resultbasename = pathinfo($result, PATHINFO_FILENAME);
251 if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
252 $number = $matches[2] + 1;
257 // Constructing the new filename.
258 $newfilename = $basename . ' (' . $number . ')';
259 if (isset($pathinfo['extension'])) {
260 $newfilename .= '.' . $pathinfo['extension'];
267 * Generates a preview image for the stored file
269 * @param stored_file $file the file we want to preview
270 * @param string $mode preview mode, eg. 'thumb'
271 * @return stored_file|bool the newly created preview file or false
273 protected function create_file_preview(stored_file $file, $mode) {
275 $mimetype = $file->get_mimetype();
277 if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
278 // make a preview of the image
279 $data = $this->create_imagefile_preview($file, $mode);
282 // unable to create the preview of this mimetype yet
290 // getimagesizefromstring() is available from PHP 5.4 but we need to support
291 // lower versions, so...
292 $tmproot = make_temp_directory('thumbnails');
293 $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
294 file_put_contents($tmpfilepath, $data);
295 $imageinfo = getimagesize($tmpfilepath);
296 unlink($tmpfilepath);
298 $context = context_system::instance();
301 'contextid' => $context->id,
302 'component' => 'core',
303 'filearea' => 'preview',
305 'filepath' => '/' . trim($mode, '/') . '/',
306 'filename' => $file->get_contenthash(),
310 $record['mimetype'] = $imageinfo['mime'];
313 return $this->create_file_from_string($record, $data);
317 * Generates a preview for the stored image file
319 * @param stored_file $file the image we want to preview
320 * @param string $mode preview mode, eg. 'thumb'
321 * @return string|bool false if a problem occurs, the thumbnail image data otherwise
323 protected function create_imagefile_preview(stored_file $file, $mode) {
325 require_once($CFG->libdir.'/gdlib.php');
327 $tmproot = make_temp_directory('thumbnails');
328 $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
329 $file->copy_content_to($tmpfilepath);
331 if ($mode === 'tinyicon') {
332 $data = generate_image_thumbnail($tmpfilepath, 24, 24);
334 } else if ($mode === 'thumb') {
335 $data = generate_image_thumbnail($tmpfilepath, 90, 90);
337 } else if ($mode === 'bigthumb') {
338 $data = generate_image_thumbnail($tmpfilepath, 250, 250);
341 throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
344 unlink($tmpfilepath);
350 * Fetch file using local file id.
352 * Please do not rely on file ids, it is usually easier to use
353 * pathname hashes instead.
355 * @param int $fileid file ID
356 * @return stored_file|bool stored_file instance if exists, false if not
358 public function get_file_by_id($fileid) {
361 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
363 LEFT JOIN {files_reference} r
364 ON f.referencefileid = r.id
366 if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
367 return $this->get_file_instance($filerecord);
374 * Fetch file using local file full pathname hash
376 * @param string $pathnamehash path name hash
377 * @return stored_file|bool stored_file instance if exists, false if not
379 public function get_file_by_hash($pathnamehash) {
382 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
384 LEFT JOIN {files_reference} r
385 ON f.referencefileid = r.id
386 WHERE f.pathnamehash = ?";
387 if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
388 return $this->get_file_instance($filerecord);
395 * Fetch locally stored file.
397 * @param int $contextid context ID
398 * @param string $component component
399 * @param string $filearea file area
400 * @param int $itemid item ID
401 * @param string $filepath file path
402 * @param string $filename file name
403 * @return stored_file|bool stored_file instance if exists, false if not
405 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
406 $filepath = clean_param($filepath, PARAM_PATH);
407 $filename = clean_param($filename, PARAM_FILE);
409 if ($filename === '') {
413 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
414 return $this->get_file_by_hash($pathnamehash);
418 * Are there any files (or directories)
420 * @param int $contextid context ID
421 * @param string $component component
422 * @param string $filearea file area
423 * @param bool|int $itemid item id or false if all items
424 * @param bool $ignoredirs whether or not ignore directories
427 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
430 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
431 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
433 if ($itemid !== false) {
434 $params['itemid'] = $itemid;
435 $where .= " AND itemid = :itemid";
441 WHERE $where AND filename <> '.'";
445 WHERE $where AND (filename <> '.' OR filepath <> '/')";
448 return !$DB->record_exists_sql($sql, $params);
452 * Returns all files belonging to given repository
454 * @param int $repositoryid
455 * @param string $sort A fragment of SQL to use for sorting
457 public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
459 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
461 LEFT JOIN {files_reference} r
462 ON f.referencefileid = r.id
463 WHERE r.repositoryid = ?";
465 $sql .= " ORDER BY {$sort}";
469 $filerecords = $DB->get_records_sql($sql, array($repositoryid));
470 foreach ($filerecords as $filerecord) {
471 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
477 * Returns all area files (optionally limited by itemid)
479 * @param int $contextid context ID
480 * @param string $component component
481 * @param string $filearea file area
482 * @param int $itemid item ID or all files if not specified
483 * @param string $sort A fragment of SQL to use for sorting
484 * @param bool $includedirs whether or not include directories
485 * @return array of stored_files indexed by pathanmehash
487 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
490 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
491 if ($itemid !== false) {
492 $itemidsql = ' AND f.itemid = :itemid ';
493 $conditions['itemid'] = $itemid;
498 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
500 LEFT JOIN {files_reference} r
501 ON f.referencefileid = r.id
502 WHERE f.contextid = :contextid
503 AND f.component = :component
504 AND f.filearea = :filearea
507 $sql .= " ORDER BY {$sort}";
511 $filerecords = $DB->get_records_sql($sql, $conditions);
512 foreach ($filerecords as $filerecord) {
513 if (!$includedirs and $filerecord->filename === '.') {
516 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
522 * Returns array based tree structure of area files
524 * @param int $contextid context ID
525 * @param string $component component
526 * @param string $filearea file area
527 * @param int $itemid item ID
528 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
530 public function get_area_tree($contextid, $component, $filearea, $itemid) {
531 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
532 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
533 // first create directory structure
534 foreach ($files as $hash=>$dir) {
535 if (!$dir->is_directory()) {
538 unset($files[$hash]);
539 if ($dir->get_filepath() === '/') {
540 $result['dirfile'] = $dir;
543 $parts = explode('/', trim($dir->get_filepath(),'/'));
545 foreach ($parts as $part) {
549 if (!isset($pointer['subdirs'][$part])) {
550 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
552 $pointer =& $pointer['subdirs'][$part];
554 $pointer['dirfile'] = $dir;
557 foreach ($files as $hash=>$file) {
558 $parts = explode('/', trim($file->get_filepath(),'/'));
560 foreach ($parts as $part) {
564 $pointer =& $pointer['subdirs'][$part];
566 $pointer['files'][$file->get_filename()] = $file;
569 $result = $this->sort_area_tree($result);
574 * Sorts the result of {@link file_storage::get_area_tree()}.
576 * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
577 * @return array of sorted results
579 protected function sort_area_tree($tree) {
580 foreach ($tree as $key => &$value) {
581 if ($key == 'subdirs') {
582 $value = $this->sort_area_tree($value);
583 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
584 } else if ($key == 'files') {
585 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
592 * Returns all files and optionally directories
594 * @param int $contextid context ID
595 * @param string $component component
596 * @param string $filearea file area
597 * @param int $itemid item ID
598 * @param int $filepath directory path
599 * @param bool $recursive include all subdirectories
600 * @param bool $includedirs include files and directories
601 * @param string $sort A fragment of SQL to use for sorting
602 * @return array of stored_files indexed by pathanmehash
604 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
607 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
611 $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
615 $dirs = $includedirs ? "" : "AND filename <> '.'";
616 $length = textlib::strlen($filepath);
618 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
620 LEFT JOIN {files_reference} r
621 ON f.referencefileid = r.id
622 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
623 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
627 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
631 $filerecords = $DB->get_records_sql($sql, $params);
632 foreach ($filerecords as $filerecord) {
633 if ($filerecord->filename == '.') {
634 $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
636 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
639 $result = array_merge($dirs, $files);
643 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
645 $length = textlib::strlen($filepath);
648 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
650 LEFT JOIN {files_reference} r
651 ON f.referencefileid = r.id
652 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
653 AND f.itemid = :itemid AND f.filename = '.'
654 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
657 $reqlevel = substr_count($filepath, '/') + 1;
658 $filerecords = $DB->get_records_sql($sql, $params);
659 foreach ($filerecords as $filerecord) {
660 if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
663 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
667 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
669 LEFT JOIN {files_reference} r
670 ON f.referencefileid = r.id
671 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
672 AND f.filepath = :filepath AND f.filename <> '.'
675 $filerecords = $DB->get_records_sql($sql, $params);
676 foreach ($filerecords as $filerecord) {
677 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
685 * Delete all area files (optionally limited by itemid).
687 * @param int $contextid context ID
688 * @param string $component component
689 * @param string $filearea file area or all areas in context if not specified
690 * @param int $itemid item ID or all files if not specified
691 * @return bool success
693 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
696 $conditions = array('contextid'=>$contextid);
697 if ($component !== false) {
698 $conditions['component'] = $component;
700 if ($filearea !== false) {
701 $conditions['filearea'] = $filearea;
703 if ($itemid !== false) {
704 $conditions['itemid'] = $itemid;
707 $filerecords = $DB->get_records('files', $conditions);
708 foreach ($filerecords as $filerecord) {
709 $this->get_file_instance($filerecord)->delete();
712 return true; // BC only
716 * Delete all the files from certain areas where itemid is limited by an
717 * arbitrary bit of SQL.
719 * @param int $contextid the id of the context the files belong to. Must be given.
720 * @param string $component the owning component. Must be given.
721 * @param string $filearea the file area name. Must be given.
722 * @param string $itemidstest an SQL fragment that the itemid must match. Used
723 * in the query like WHERE itemid $itemidstest. Must used named parameters,
724 * and may not used named parameters called contextid, component or filearea.
725 * @param array $params any query params used by $itemidstest.
727 public function delete_area_files_select($contextid, $component,
728 $filearea, $itemidstest, array $params = null) {
731 $where = "contextid = :contextid
732 AND component = :component
733 AND filearea = :filearea
734 AND itemid $itemidstest";
735 $params['contextid'] = $contextid;
736 $params['component'] = $component;
737 $params['filearea'] = $filearea;
739 $filerecords = $DB->get_recordset_select('files', $where, $params);
740 foreach ($filerecords as $filerecord) {
741 $this->get_file_instance($filerecord)->delete();
743 $filerecords->close();
747 * Move all the files in a file area from one context to another.
749 * @param int $oldcontextid the context the files are being moved from.
750 * @param int $newcontextid the context the files are being moved to.
751 * @param string $component the plugin that these files belong to.
752 * @param string $filearea the name of the file area.
753 * @param int $itemid file item ID
754 * @return int the number of files moved, for information.
756 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
757 // Note, this code is based on some code that Petr wrote in
758 // forum_move_attachments in mod/forum/lib.php. I moved it here because
759 // I needed it in the question code too.
762 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
763 foreach ($oldfiles as $oldfile) {
764 $filerecord = new stdClass();
765 $filerecord->contextid = $newcontextid;
766 $this->create_file_from_storedfile($filerecord, $oldfile);
771 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
778 * Recursively creates directory.
780 * @param int $contextid context ID
781 * @param string $component component
782 * @param string $filearea file area
783 * @param int $itemid item ID
784 * @param string $filepath file path
785 * @param int $userid the user ID
786 * @return bool success
788 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
791 // validate all parameters, we do not want any rubbish stored in database, right?
792 if (!is_number($contextid) or $contextid < 1) {
793 throw new file_exception('storedfileproblem', 'Invalid contextid');
796 $component = clean_param($component, PARAM_COMPONENT);
797 if (empty($component)) {
798 throw new file_exception('storedfileproblem', 'Invalid component');
801 $filearea = clean_param($filearea, PARAM_AREA);
802 if (empty($filearea)) {
803 throw new file_exception('storedfileproblem', 'Invalid filearea');
806 if (!is_number($itemid) or $itemid < 0) {
807 throw new file_exception('storedfileproblem', 'Invalid itemid');
810 $filepath = clean_param($filepath, PARAM_PATH);
811 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
812 // path must start and end with '/'
813 throw new file_exception('storedfileproblem', 'Invalid file path');
816 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
818 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
822 static $contenthash = null;
824 $this->add_string_to_pool('');
825 $contenthash = sha1('');
830 $dir_record = new stdClass();
831 $dir_record->contextid = $contextid;
832 $dir_record->component = $component;
833 $dir_record->filearea = $filearea;
834 $dir_record->itemid = $itemid;
835 $dir_record->filepath = $filepath;
836 $dir_record->filename = '.';
837 $dir_record->contenthash = $contenthash;
838 $dir_record->filesize = 0;
840 $dir_record->timecreated = $now;
841 $dir_record->timemodified = $now;
842 $dir_record->mimetype = null;
843 $dir_record->userid = $userid;
845 $dir_record->pathnamehash = $pathnamehash;
847 $DB->insert_record('files', $dir_record);
848 $dir_info = $this->get_file_by_hash($pathnamehash);
850 if ($filepath !== '/') {
851 //recurse to parent dirs
852 $filepath = trim($filepath, '/');
853 $filepath = explode('/', $filepath);
854 array_pop($filepath);
855 $filepath = implode('/', $filepath);
856 $filepath = ($filepath === '') ? '/' : "/$filepath/";
857 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
864 * Add new local file based on existing local file.
866 * @param stdClass|array $filerecord object or array describing changes
867 * @param stored_file|int $fileorid id or stored_file instance of the existing local file
868 * @return stored_file instance of newly created file
870 public function create_file_from_storedfile($filerecord, $fileorid) {
873 if ($fileorid instanceof stored_file) {
874 $fid = $fileorid->get_id();
879 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
881 unset($filerecord['id']);
882 unset($filerecord['filesize']);
883 unset($filerecord['contenthash']);
884 unset($filerecord['pathnamehash']);
886 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
888 LEFT JOIN {files_reference} r
889 ON f.referencefileid = r.id
892 if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
893 throw new file_exception('storedfileproblem', 'File does not exist');
896 unset($newrecord->id);
898 foreach ($filerecord as $key => $value) {
899 // validate all parameters, we do not want any rubbish stored in database, right?
900 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
901 throw new file_exception('storedfileproblem', 'Invalid contextid');
904 if ($key == 'component') {
905 $value = clean_param($value, PARAM_COMPONENT);
907 throw new file_exception('storedfileproblem', 'Invalid component');
911 if ($key == 'filearea') {
912 $value = clean_param($value, PARAM_AREA);
914 throw new file_exception('storedfileproblem', 'Invalid filearea');
918 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
919 throw new file_exception('storedfileproblem', 'Invalid itemid');
923 if ($key == 'filepath') {
924 $value = clean_param($value, PARAM_PATH);
925 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
926 // path must start and end with '/'
927 throw new file_exception('storedfileproblem', 'Invalid file path');
931 if ($key == 'filename') {
932 $value = clean_param($value, PARAM_FILE);
934 // path must start and end with '/'
935 throw new file_exception('storedfileproblem', 'Invalid file name');
939 if ($key === 'timecreated' or $key === 'timemodified') {
940 if (!is_number($value)) {
941 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
944 //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)
949 if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
950 $value = clean_param($value, PARAM_INT);
953 $newrecord->$key = $value;
956 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
958 if ($newrecord->filename === '.') {
959 // special case - only this function supports directories ;-)
960 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
961 // update the existing directory with the new data
962 $newrecord->id = $directory->get_id();
963 $DB->update_record('files', $newrecord);
964 return $this->get_file_instance($newrecord);
967 // note: referencefileid is copied from the original file so that
968 // creating a new file from an existing alias creates new alias implicitly.
969 // here we just check the database consistency.
970 if (!empty($newrecord->repositoryid)) {
971 if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
972 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
977 $newrecord->id = $DB->insert_record('files', $newrecord);
978 } catch (dml_exception $e) {
979 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
980 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
984 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
986 return $this->get_file_instance($newrecord);
990 * Add new local file.
992 * @param stdClass|array $filerecord object or array describing file
993 * @param string $url the URL to the file
994 * @param array $options {@link download_file_content()} options
995 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
996 * @return stored_file
998 public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1000 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1001 $filerecord = (object)$filerecord; // We support arrays too.
1003 $headers = isset($options['headers']) ? $options['headers'] : null;
1004 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
1005 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
1006 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
1007 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1008 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1009 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
1011 if (!isset($filerecord->filename)) {
1012 $parts = explode('/', $url);
1013 $filename = array_pop($parts);
1014 $filerecord->filename = clean_param($filename, PARAM_FILE);
1016 $source = !empty($filerecord->source) ? $filerecord->source : $url;
1017 $filerecord->source = clean_param($source, PARAM_URL);
1020 check_dir_exists($this->tempdir);
1021 $tmpfile = tempnam($this->tempdir, 'newfromurl');
1022 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1023 if ($content === false) {
1024 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1027 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1030 } catch (Exception $e) {
1036 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1037 if ($content === false) {
1038 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1040 return $this->create_file_from_string($filerecord, $content);
1045 * Add new local file.
1047 * @param stdClass|array $filerecord object or array describing file
1048 * @param string $pathname path to file or content of file
1049 * @return stored_file
1051 public function create_file_from_pathname($filerecord, $pathname) {
1054 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1055 $filerecord = (object)$filerecord; // We support arrays too.
1057 // validate all parameters, we do not want any rubbish stored in database, right?
1058 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1059 throw new file_exception('storedfileproblem', 'Invalid contextid');
1062 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1063 if (empty($filerecord->component)) {
1064 throw new file_exception('storedfileproblem', 'Invalid component');
1067 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1068 if (empty($filerecord->filearea)) {
1069 throw new file_exception('storedfileproblem', 'Invalid filearea');
1072 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1073 throw new file_exception('storedfileproblem', 'Invalid itemid');
1076 if (!empty($filerecord->sortorder)) {
1077 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1078 $filerecord->sortorder = 0;
1081 $filerecord->sortorder = 0;
1084 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1085 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1086 // path must start and end with '/'
1087 throw new file_exception('storedfileproblem', 'Invalid file path');
1090 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1091 if ($filerecord->filename === '') {
1092 // filename must not be empty
1093 throw new file_exception('storedfileproblem', 'Invalid file name');
1097 if (isset($filerecord->timecreated)) {
1098 if (!is_number($filerecord->timecreated)) {
1099 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1101 if ($filerecord->timecreated < 0) {
1102 //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)
1103 $filerecord->timecreated = 0;
1106 $filerecord->timecreated = $now;
1109 if (isset($filerecord->timemodified)) {
1110 if (!is_number($filerecord->timemodified)) {
1111 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1113 if ($filerecord->timemodified < 0) {
1114 //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)
1115 $filerecord->timemodified = 0;
1118 $filerecord->timemodified = $now;
1121 $newrecord = new stdClass();
1123 $newrecord->contextid = $filerecord->contextid;
1124 $newrecord->component = $filerecord->component;
1125 $newrecord->filearea = $filerecord->filearea;
1126 $newrecord->itemid = $filerecord->itemid;
1127 $newrecord->filepath = $filerecord->filepath;
1128 $newrecord->filename = $filerecord->filename;
1130 $newrecord->timecreated = $filerecord->timecreated;
1131 $newrecord->timemodified = $filerecord->timemodified;
1132 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1133 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1134 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1135 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1136 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1137 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1138 $newrecord->sortorder = $filerecord->sortorder;
1140 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1142 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1145 $newrecord->id = $DB->insert_record('files', $newrecord);
1146 } catch (dml_exception $e) {
1148 $this->deleted_file_cleanup($newrecord->contenthash);
1150 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1151 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1154 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1156 return $this->get_file_instance($newrecord);
1160 * Add new local file.
1162 * @param stdClass|array $filerecord object or array describing file
1163 * @param string $content content of file
1164 * @return stored_file
1166 public function create_file_from_string($filerecord, $content) {
1169 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1170 $filerecord = (object)$filerecord; // We support arrays too.
1172 // validate all parameters, we do not want any rubbish stored in database, right?
1173 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1174 throw new file_exception('storedfileproblem', 'Invalid contextid');
1177 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1178 if (empty($filerecord->component)) {
1179 throw new file_exception('storedfileproblem', 'Invalid component');
1182 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1183 if (empty($filerecord->filearea)) {
1184 throw new file_exception('storedfileproblem', 'Invalid filearea');
1187 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1188 throw new file_exception('storedfileproblem', 'Invalid itemid');
1191 if (!empty($filerecord->sortorder)) {
1192 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1193 $filerecord->sortorder = 0;
1196 $filerecord->sortorder = 0;
1199 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1200 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1201 // path must start and end with '/'
1202 throw new file_exception('storedfileproblem', 'Invalid file path');
1205 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1206 if ($filerecord->filename === '') {
1207 // path must start and end with '/'
1208 throw new file_exception('storedfileproblem', 'Invalid file name');
1212 if (isset($filerecord->timecreated)) {
1213 if (!is_number($filerecord->timecreated)) {
1214 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1216 if ($filerecord->timecreated < 0) {
1217 //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)
1218 $filerecord->timecreated = 0;
1221 $filerecord->timecreated = $now;
1224 if (isset($filerecord->timemodified)) {
1225 if (!is_number($filerecord->timemodified)) {
1226 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1228 if ($filerecord->timemodified < 0) {
1229 //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)
1230 $filerecord->timemodified = 0;
1233 $filerecord->timemodified = $now;
1236 $newrecord = new stdClass();
1238 $newrecord->contextid = $filerecord->contextid;
1239 $newrecord->component = $filerecord->component;
1240 $newrecord->filearea = $filerecord->filearea;
1241 $newrecord->itemid = $filerecord->itemid;
1242 $newrecord->filepath = $filerecord->filepath;
1243 $newrecord->filename = $filerecord->filename;
1245 $newrecord->timecreated = $filerecord->timecreated;
1246 $newrecord->timemodified = $filerecord->timemodified;
1247 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1248 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1249 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1250 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1251 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1252 $newrecord->sortorder = $filerecord->sortorder;
1254 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1255 $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1256 // get mimetype by magic bytes
1257 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1259 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1262 $newrecord->id = $DB->insert_record('files', $newrecord);
1263 } catch (dml_exception $e) {
1265 $this->deleted_file_cleanup($newrecord->contenthash);
1267 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1268 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1271 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1273 return $this->get_file_instance($newrecord);
1277 * Create a new alias/shortcut file from file reference information
1279 * @param stdClass|array $filerecord object or array describing the new file
1280 * @param int $repositoryid the id of the repository that provides the original file
1281 * @param string $reference the information required by the repository to locate the original file
1282 * @param array $options options for creating the new file
1283 * @return stored_file
1285 public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1288 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1289 $filerecord = (object)$filerecord; // We support arrays too.
1291 // validate all parameters, we do not want any rubbish stored in database, right?
1292 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1293 throw new file_exception('storedfileproblem', 'Invalid contextid');
1296 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1297 if (empty($filerecord->component)) {
1298 throw new file_exception('storedfileproblem', 'Invalid component');
1301 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1302 if (empty($filerecord->filearea)) {
1303 throw new file_exception('storedfileproblem', 'Invalid filearea');
1306 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1307 throw new file_exception('storedfileproblem', 'Invalid itemid');
1310 if (!empty($filerecord->sortorder)) {
1311 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1312 $filerecord->sortorder = 0;
1315 $filerecord->sortorder = 0;
1318 // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
1319 unset($filerecord->referencelastsync);
1320 unset($filerecord->referencelifetime);
1322 $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1323 $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1324 $filerecord->source = empty($filerecord->source) ? null : $filerecord->source;
1325 $filerecord->author = empty($filerecord->author) ? null : $filerecord->author;
1326 $filerecord->license = empty($filerecord->license) ? null : $filerecord->license;
1327 $filerecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
1328 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1329 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1330 // Path must start and end with '/'.
1331 throw new file_exception('storedfileproblem', 'Invalid file path');
1334 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1335 if ($filerecord->filename === '') {
1336 // Path must start and end with '/'.
1337 throw new file_exception('storedfileproblem', 'Invalid file name');
1341 if (isset($filerecord->timecreated)) {
1342 if (!is_number($filerecord->timecreated)) {
1343 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1345 if ($filerecord->timecreated < 0) {
1346 // 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)
1347 $filerecord->timecreated = 0;
1350 $filerecord->timecreated = $now;
1353 if (isset($filerecord->timemodified)) {
1354 if (!is_number($filerecord->timemodified)) {
1355 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1357 if ($filerecord->timemodified < 0) {
1358 // 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)
1359 $filerecord->timemodified = 0;
1362 $filerecord->timemodified = $now;
1365 $transaction = $DB->start_delegated_transaction();
1368 $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1369 } catch (Exception $e) {
1370 throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1373 if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1374 // there was specified the contenthash for a file already stored in moodle filepool
1375 if (empty($filerecord->filesize)) {
1376 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1377 $filerecord->filesize = filesize($filepathname);
1379 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1382 // atempt to get the result of last synchronisation for this reference
1383 $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1384 'id, contenthash, filesize', IGNORE_MULTIPLE);
1386 $filerecord->contenthash = $lastcontent->contenthash;
1387 $filerecord->filesize = $lastcontent->filesize;
1389 // External file doesn't have content in moodle.
1390 // So we create an empty file for it.
1391 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1395 $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1398 $filerecord->id = $DB->insert_record('files', $filerecord);
1399 } catch (dml_exception $e) {
1400 if (!empty($newfile)) {
1401 $this->deleted_file_cleanup($filerecord->contenthash);
1403 throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1404 $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1407 $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1409 $transaction->allow_commit();
1411 // this will retrieve all reference information from DB as well
1412 return $this->get_file_by_id($filerecord->id);
1416 * Creates new image file from existing.
1418 * @param stdClass|array $filerecord object or array describing new file
1419 * @param int|stored_file $fid file id or stored file object
1420 * @param int $newwidth in pixels
1421 * @param int $newheight in pixels
1422 * @param bool $keepaspectratio whether or not keep aspect ratio
1423 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1424 * @return stored_file
1426 public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1427 if (!function_exists('imagecreatefromstring')) {
1428 //Most likely the GD php extension isn't installed
1429 //image conversion cannot succeed
1430 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1433 if ($fid instanceof stored_file) {
1434 $fid = $fid->get_id();
1437 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1439 if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1440 throw new file_exception('storedfileproblem', 'File does not exist');
1443 if (!$imageinfo = $file->get_imageinfo()) {
1444 throw new file_exception('storedfileproblem', 'File is not an image');
1447 if (!isset($filerecord['filename'])) {
1448 $filerecord['filename'] = $file->get_filename();
1451 if (!isset($filerecord['mimetype'])) {
1452 $filerecord['mimetype'] = $imageinfo['mimetype'];
1455 $width = $imageinfo['width'];
1456 $height = $imageinfo['height'];
1457 $mimetype = $imageinfo['mimetype'];
1459 if ($keepaspectratio) {
1460 if (0 >= $newwidth and 0 >= $newheight) {
1461 // no sizes specified
1463 $newheight = $height;
1465 } else if (0 < $newwidth and 0 < $newheight) {
1466 $xheight = ($newwidth*($height/$width));
1467 if ($xheight < $newheight) {
1468 $newheight = (int)$xheight;
1470 $newwidth = (int)($newheight*($width/$height));
1473 } else if (0 < $newwidth) {
1474 $newheight = (int)($newwidth*($height/$width));
1476 } else { //0 < $newheight
1477 $newwidth = (int)($newheight*($width/$height));
1481 if (0 >= $newwidth) {
1484 if (0 >= $newheight) {
1485 $newheight = $height;
1489 $img = imagecreatefromstring($file->get_content());
1490 if ($height != $newheight or $width != $newwidth) {
1491 $newimg = imagecreatetruecolor($newwidth, $newheight);
1492 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1494 throw new file_exception('storedfileproblem', 'Can not resize image');
1501 switch ($filerecord['mimetype']) {
1507 if (is_null($quality)) {
1510 imagejpeg($img, NULL, $quality);
1515 $quality = (int)$quality;
1516 imagepng($img, NULL, $quality, NULL);
1520 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1523 $content = ob_get_contents();
1528 throw new file_exception('storedfileproblem', 'Can not convert image');
1531 return $this->create_file_from_string($filerecord, $content);
1535 * Add file content to sha1 pool.
1537 * @param string $pathname path to file
1538 * @param string $contenthash sha1 hash of content if known (performance only)
1539 * @return array (contenthash, filesize, newfile)
1541 public function add_file_to_pool($pathname, $contenthash = NULL) {
1542 if (!is_readable($pathname)) {
1543 throw new file_exception('storedfilecannotread', '', $pathname);
1546 if (is_null($contenthash)) {
1547 $contenthash = sha1_file($pathname);
1550 $filesize = filesize($pathname);
1552 $hashpath = $this->path_from_hash($contenthash);
1553 $hashfile = "$hashpath/$contenthash";
1555 if (file_exists($hashfile)) {
1556 if (filesize($hashfile) !== $filesize) {
1557 throw new file_pool_content_exception($contenthash);
1562 if (!is_dir($hashpath)) {
1563 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1564 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1569 if (!copy($pathname, $hashfile)) {
1570 throw new file_exception('storedfilecannotread', '', $pathname);
1573 if (filesize($hashfile) !== $filesize) {
1575 throw new file_pool_content_exception($contenthash);
1577 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1581 return array($contenthash, $filesize, $newfile);
1585 * Add string content to sha1 pool.
1587 * @param string $content file content - binary string
1588 * @return array (contenthash, filesize, newfile)
1590 public function add_string_to_pool($content) {
1591 $contenthash = sha1($content);
1592 $filesize = strlen($content); // binary length
1594 $hashpath = $this->path_from_hash($contenthash);
1595 $hashfile = "$hashpath/$contenthash";
1598 if (file_exists($hashfile)) {
1599 if (filesize($hashfile) !== $filesize) {
1600 throw new file_pool_content_exception($contenthash);
1605 if (!is_dir($hashpath)) {
1606 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1607 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1612 file_put_contents($hashfile, $content);
1614 if (filesize($hashfile) !== $filesize) {
1616 throw new file_pool_content_exception($contenthash);
1618 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1621 return array($contenthash, $filesize, $newfile);
1625 * Serve file content using X-Sendfile header.
1626 * Please make sure that all headers are already sent
1627 * and the all access control checks passed.
1629 * @param string $contenthash sah1 hash of the file content to be served
1630 * @return bool success
1632 public function xsendfile($contenthash) {
1634 require_once("$CFG->libdir/xsendfilelib.php");
1636 $hashpath = $this->path_from_hash($contenthash);
1637 return xsendfile("$hashpath/$contenthash");
1643 * @param string $contenthash
1646 public function content_exists($contenthash) {
1647 $dir = $this->path_from_hash($contenthash);
1648 $filepath = $dir . '/' . $contenthash;
1649 return file_exists($filepath);
1653 * Return path to file with given hash.
1655 * NOTE: must not be public, files in pool must not be modified
1657 * @param string $contenthash content hash
1658 * @return string expected file location
1660 protected function path_from_hash($contenthash) {
1661 $l1 = $contenthash[0].$contenthash[1];
1662 $l2 = $contenthash[2].$contenthash[3];
1663 return "$this->filedir/$l1/$l2";
1667 * Return path to file with given hash.
1669 * NOTE: must not be public, files in pool must not be modified
1671 * @param string $contenthash content hash
1672 * @return string expected file location
1674 protected function trash_path_from_hash($contenthash) {
1675 $l1 = $contenthash[0].$contenthash[1];
1676 $l2 = $contenthash[2].$contenthash[3];
1677 return "$this->trashdir/$l1/$l2";
1681 * Tries to recover missing content of file from trash.
1683 * @param stored_file $file stored_file instance
1684 * @return bool success
1686 public function try_content_recovery($file) {
1687 $contenthash = $file->get_contenthash();
1688 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1689 if (!is_readable($trashfile)) {
1690 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1693 // nice, at least alternative trash file in trash root exists
1694 $trashfile = $this->trashdir.'/'.$contenthash;
1696 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1697 //weird, better fail early
1700 $contentdir = $this->path_from_hash($contenthash);
1701 $contentfile = $contentdir.'/'.$contenthash;
1702 if (file_exists($contentfile)) {
1703 //strange, no need to recover anything
1706 if (!is_dir($contentdir)) {
1707 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1711 return rename($trashfile, $contentfile);
1715 * Marks pool file as candidate for deleting.
1717 * DO NOT call directly - reserved for core!!
1719 * @param string $contenthash
1721 public function deleted_file_cleanup($contenthash) {
1724 if ($contenthash === sha1('')) {
1725 // No need to delete empty content file with sha1('') content hash.
1729 //Note: this section is critical - in theory file could be reused at the same
1730 // time, if this happens we can still recover the file from trash
1731 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1732 // file content is still used
1735 //move content file to trash
1736 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1737 if (!file_exists($contentfile)) {
1738 //weird, but no problem
1741 $trashpath = $this->trash_path_from_hash($contenthash);
1742 $trashfile = $trashpath.'/'.$contenthash;
1743 if (file_exists($trashfile)) {
1744 // we already have this content in trash, no need to move it there
1745 unlink($contentfile);
1748 if (!is_dir($trashpath)) {
1749 mkdir($trashpath, $this->dirpermissions, true);
1751 rename($contentfile, $trashfile);
1752 chmod($trashfile, $this->filepermissions); // fix permissions if needed
1756 * When user referring to a moodle file, we build the reference field
1758 * @param array $params
1761 public static function pack_reference($params) {
1762 $params = (array)$params;
1763 $reference = array();
1764 $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1765 $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1766 $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT);
1767 $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA);
1768 $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);
1769 $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE);
1770 return base64_encode(serialize($reference));
1774 * Unpack reference field
1776 * @param string $str
1777 * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1778 * @throws file_reference_exception if the $str does not have the expected format
1781 public static function unpack_reference($str, $cleanparams = false) {
1782 $decoded = base64_decode($str, true);
1783 if ($decoded === false) {
1784 throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1786 $params = @unserialize($decoded); // hide E_NOTICE
1787 if ($params === false) {
1788 throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1790 if (is_array($params) && $cleanparams) {
1792 'component' => is_null($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT),
1793 'filearea' => is_null($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA),
1794 'itemid' => is_null($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT),
1795 'filename' => is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE),
1796 'filepath' => is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH),
1797 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1804 * Returns all aliases that refer to some stored_file via the given reference
1806 * All repositories that provide access to a stored_file are expected to use
1807 * {@link self::pack_reference()}. This method can't be used if the given reference
1808 * does not use this format or if you are looking for references to an external file
1809 * (for example it can't be used to search for all aliases that refer to a given
1810 * Dropbox or Box.net file).
1812 * Aliases in user draft areas are excluded from the returned list.
1814 * @param string $reference identification of the referenced file
1815 * @return array of stored_file indexed by its pathnamehash
1817 public function search_references($reference) {
1820 if (is_null($reference)) {
1821 throw new coding_exception('NULL is not a valid reference to an external file');
1824 // Give {@link self::unpack_reference()} a chance to throw exception if the
1825 // reference is not in a valid format.
1826 self::unpack_reference($reference);
1828 $referencehash = sha1($reference);
1830 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1832 JOIN {files_reference} r ON f.referencefileid = r.id
1833 JOIN {repository_instances} ri ON r.repositoryid = ri.id
1834 WHERE r.referencehash = ?
1835 AND (f.component <> ? OR f.filearea <> ?)";
1837 $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1839 foreach ($rs as $filerecord) {
1840 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1847 * Returns the number of aliases that refer to some stored_file via the given reference
1849 * All repositories that provide access to a stored_file are expected to use
1850 * {@link self::pack_reference()}. This method can't be used if the given reference
1851 * does not use this format or if you are looking for references to an external file
1852 * (for example it can't be used to count aliases that refer to a given Dropbox or
1855 * Aliases in user draft areas are not counted.
1857 * @param string $reference identification of the referenced file
1860 public function search_references_count($reference) {
1863 if (is_null($reference)) {
1864 throw new coding_exception('NULL is not a valid reference to an external file');
1867 // Give {@link self::unpack_reference()} a chance to throw exception if the
1868 // reference is not in a valid format.
1869 self::unpack_reference($reference);
1871 $referencehash = sha1($reference);
1873 $sql = "SELECT COUNT(f.id)
1875 JOIN {files_reference} r ON f.referencefileid = r.id
1876 JOIN {repository_instances} ri ON r.repositoryid = ri.id
1877 WHERE r.referencehash = ?
1878 AND (f.component <> ? OR f.filearea <> ?)";
1880 return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
1884 * Returns all aliases that link to the given stored_file
1886 * Aliases in user draft areas are excluded from the returned list.
1888 * @param stored_file $storedfile
1889 * @return array of stored_file
1891 public function get_references_by_storedfile(stored_file $storedfile) {
1895 $params['contextid'] = $storedfile->get_contextid();
1896 $params['component'] = $storedfile->get_component();
1897 $params['filearea'] = $storedfile->get_filearea();
1898 $params['itemid'] = $storedfile->get_itemid();
1899 $params['filename'] = $storedfile->get_filename();
1900 $params['filepath'] = $storedfile->get_filepath();
1902 return $this->search_references(self::pack_reference($params));
1906 * Returns the number of aliases that link to the given stored_file
1908 * Aliases in user draft areas are not counted.
1910 * @param stored_file $storedfile
1913 public function get_references_count_by_storedfile(stored_file $storedfile) {
1917 $params['contextid'] = $storedfile->get_contextid();
1918 $params['component'] = $storedfile->get_component();
1919 $params['filearea'] = $storedfile->get_filearea();
1920 $params['itemid'] = $storedfile->get_itemid();
1921 $params['filename'] = $storedfile->get_filename();
1922 $params['filepath'] = $storedfile->get_filepath();
1924 return $this->search_references_count(self::pack_reference($params));
1928 * Updates all files that are referencing this file with the new contenthash
1931 * @param stored_file $storedfile
1933 public function update_references_to_storedfile(stored_file $storedfile) {
1936 $params['contextid'] = $storedfile->get_contextid();
1937 $params['component'] = $storedfile->get_component();
1938 $params['filearea'] = $storedfile->get_filearea();
1939 $params['itemid'] = $storedfile->get_itemid();
1940 $params['filename'] = $storedfile->get_filename();
1941 $params['filepath'] = $storedfile->get_filepath();
1942 $reference = self::pack_reference($params);
1943 $referencehash = sha1($reference);
1945 $sql = "SELECT repositoryid, id FROM {files_reference}
1946 WHERE referencehash = ?";
1947 $rs = $DB->get_recordset_sql($sql, array($referencehash));
1950 foreach ($rs as $record) {
1951 require_once($CFG->dirroot.'/repository/lib.php');
1952 $repo = repository::get_instance($record->repositoryid);
1953 $lifetime = $repo->get_reference_file_lifetime($reference);
1954 $this->update_references($record->id, $now, $lifetime,
1955 $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
1961 * Convert file alias to local file
1963 * @throws moodle_exception if file could not be downloaded
1965 * @param stored_file $storedfile a stored_file instances
1966 * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
1967 * @return stored_file stored_file
1969 public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
1971 $storedfile->import_external_file_contents($maxbytes);
1972 $storedfile->delete_reference();
1977 * Return mimetype by given file pathname
1979 * If file has a known extension, we return the mimetype based on extension.
1980 * Otherwise (when possible) we try to get the mimetype from file contents.
1982 * @param string $pathname full path to the file
1983 * @param string $filename correct file name with extension, if omitted will be taken from $path
1986 public static function mimetype($pathname, $filename = null) {
1987 if (empty($filename)) {
1988 $filename = $pathname;
1990 $type = mimeinfo('type', $filename);
1991 if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1992 $finfo = new finfo(FILEINFO_MIME_TYPE);
1993 $type = mimeinfo_from_type('type', $finfo->file($pathname));
2001 public function cron() {
2004 // find out all stale draft areas (older than 4 days) and purge them
2005 // those are identified by time stamp of the /. root dir
2006 mtrace('Deleting old draft files... ', '');
2007 cron_trace_time_and_memory();
2008 $old = time() - 60*60*24*4;
2011 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2012 AND timecreated < :old";
2013 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2014 foreach ($rs as $dir) {
2015 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2020 // remove orphaned preview files (that is files in the core preview filearea without
2021 // the existing original file)
2022 mtrace('Deleting orphaned preview files... ', '');
2023 cron_trace_time_and_memory();
2026 LEFT JOIN {files} o ON (p.filename = o.contenthash)
2027 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2029 $syscontext = context_system::instance();
2030 $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2031 foreach ($rs as $orphan) {
2032 $file = $this->get_file_instance($orphan);
2033 if (!$file->is_directory()) {
2040 // remove trash pool files once a day
2041 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2042 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2043 require_once($CFG->libdir.'/filelib.php');
2044 // Delete files that are associated with a context that no longer exists.
2045 mtrace('Cleaning up files from deleted contexts... ', '');
2046 cron_trace_time_and_memory();
2047 $sql = "SELECT DISTINCT f.contextid
2049 LEFT OUTER JOIN {context} c ON f.contextid = c.id
2050 WHERE c.id IS NULL";
2051 $rs = $DB->get_recordset_sql($sql);
2053 $fs = get_file_storage();
2054 foreach ($rs as $ctx) {
2055 $fs->delete_area_files($ctx->contextid);
2061 mtrace('Deleting trash files... ', '');
2062 cron_trace_time_and_memory();
2063 fulldelete($this->trashdir);
2064 set_config('fileslastcleanup', time());
2070 * Get the sql formated fields for a file instance to be created from a
2071 * {files} and {files_refernece} join.
2073 * @param string $filesprefix the table prefix for the {files} table
2074 * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2075 * @return string the sql to go after a SELECT
2077 private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2078 // Note, these fieldnames MUST NOT overlap between the two tables,
2079 // else problems like MDL-33172 occur.
2080 $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2081 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2082 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2084 $referencefields = array('repositoryid' => 'repositoryid',
2085 'reference' => 'reference',
2086 'lastsync' => 'referencelastsync',
2087 'lifetime' => 'referencelifetime');
2089 // id is specifically named to prevent overlaping between the two tables.
2091 $fields[] = $filesprefix.'.id AS id';
2092 foreach ($filefields as $field) {
2093 $fields[] = "{$filesprefix}.{$field}";
2096 foreach ($referencefields as $field => $alias) {
2097 $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2100 return implode(', ', $fields);
2104 * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2106 * If the record already exists, its id is returned. If there is no such record yet,
2107 * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
2109 * @param int $repositoryid
2110 * @param string $reference
2113 private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2116 $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2118 if ($id !== false) {
2119 // bah, that was easy
2123 // no such record yet, create one
2125 $id = $DB->insert_record('files_reference', array(
2126 'repositoryid' => $repositoryid,
2127 'reference' => $reference,
2128 'referencehash' => sha1($reference),
2129 'lastsync' => $lastsync,
2130 'lifetime' => $lifetime));
2131 } catch (dml_exception $e) {
2132 // if inserting the new record failed, chances are that the race condition has just
2133 // occured and the unique index did not allow to create the second record with the same
2134 // repositoryid + reference combo
2135 $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2142 * Returns the id of the record in {files_reference} that matches the passed parameters
2144 * Depending on the required strictness, false can be returned. The behaviour is consistent
2145 * with standard DML methods.
2147 * @param int $repositoryid
2148 * @param string $reference
2149 * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2152 private function get_referencefileid($repositoryid, $reference, $strictness) {
2155 return $DB->get_field('files_reference', 'id',
2156 array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2160 * Updates a reference to the external resource and all files that use it
2162 * This function is called after synchronisation of an external file and updates the
2163 * contenthash, filesize and status of all files that reference this external file
2164 * as well as time last synchronised and sync lifetime (how long we don't need to call
2165 * synchronisation for this reference).
2167 * @param int $referencefileid
2168 * @param int $lastsync
2169 * @param int $lifetime
2170 * @param string $contenthash
2171 * @param int $filesize
2172 * @param int $status 0 if ok or 666 if source is missing
2174 public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2176 $referencefileid = clean_param($referencefileid, PARAM_INT);
2177 $lastsync = clean_param($lastsync, PARAM_INT);
2178 $lifetime = clean_param($lifetime, PARAM_INT);
2179 validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2180 $filesize = clean_param($filesize, PARAM_INT);
2181 $status = clean_param($status, PARAM_INT);
2182 $params = array('contenthash' => $contenthash,
2183 'filesize' => $filesize,
2184 'status' => $status,
2185 'referencefileid' => $referencefileid,
2186 'lastsync' => $lastsync,
2187 'lifetime' => $lifetime);
2188 $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2189 status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
2190 WHERE referencefileid = :referencefileid', $params);
2191 $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2192 $DB->update_record('files_reference', (object)$data);