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 * Generates a preview image for the stored file
186 * @param stored_file $file the file we want to preview
187 * @param string $mode preview mode, eg. 'thumb'
188 * @return stored_file|bool the newly created preview file or false
190 protected function create_file_preview(stored_file $file, $mode) {
192 $mimetype = $file->get_mimetype();
194 if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
195 // make a preview of the image
196 $data = $this->create_imagefile_preview($file, $mode);
199 // unable to create the preview of this mimetype yet
207 // getimagesizefromstring() is available from PHP 5.4 but we need to support
208 // lower versions, so...
209 $tmproot = make_temp_directory('thumbnails');
210 $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
211 file_put_contents($tmpfilepath, $data);
212 $imageinfo = getimagesize($tmpfilepath);
213 unlink($tmpfilepath);
215 $context = context_system::instance();
218 'contextid' => $context->id,
219 'component' => 'core',
220 'filearea' => 'preview',
222 'filepath' => '/' . trim($mode, '/') . '/',
223 'filename' => $file->get_contenthash(),
227 $record['mimetype'] = $imageinfo['mime'];
230 return $this->create_file_from_string($record, $data);
234 * Generates a preview for the stored image file
236 * @param stored_file $file the image we want to preview
237 * @param string $mode preview mode, eg. 'thumb'
238 * @return string|bool false if a problem occurs, the thumbnail image data otherwise
240 protected function create_imagefile_preview(stored_file $file, $mode) {
242 require_once($CFG->libdir.'/gdlib.php');
244 $tmproot = make_temp_directory('thumbnails');
245 $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
246 $file->copy_content_to($tmpfilepath);
248 if ($mode === 'tinyicon') {
249 $data = generate_image_thumbnail($tmpfilepath, 24, 24);
251 } else if ($mode === 'thumb') {
252 $data = generate_image_thumbnail($tmpfilepath, 90, 90);
255 throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
258 unlink($tmpfilepath);
264 * Fetch file using local file id.
266 * Please do not rely on file ids, it is usually easier to use
267 * pathname hashes instead.
269 * @param int $fileid file ID
270 * @return stored_file|bool stored_file instance if exists, false if not
272 public function get_file_by_id($fileid) {
275 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
277 LEFT JOIN {files_reference} r
278 ON f.referencefileid = r.id
280 if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
281 return $this->get_file_instance($filerecord);
288 * Fetch file using local file full pathname hash
290 * @param string $pathnamehash path name hash
291 * @return stored_file|bool stored_file instance if exists, false if not
293 public function get_file_by_hash($pathnamehash) {
296 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
298 LEFT JOIN {files_reference} r
299 ON f.referencefileid = r.id
300 WHERE f.pathnamehash = ?";
301 if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
302 return $this->get_file_instance($filerecord);
309 * Fetch locally stored file.
311 * @param int $contextid context ID
312 * @param string $component component
313 * @param string $filearea file area
314 * @param int $itemid item ID
315 * @param string $filepath file path
316 * @param string $filename file name
317 * @return stored_file|bool stored_file instance if exists, false if not
319 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
320 $filepath = clean_param($filepath, PARAM_PATH);
321 $filename = clean_param($filename, PARAM_FILE);
323 if ($filename === '') {
327 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
328 return $this->get_file_by_hash($pathnamehash);
332 * Are there any files (or directories)
334 * @param int $contextid context ID
335 * @param string $component component
336 * @param string $filearea file area
337 * @param bool|int $itemid item id or false if all items
338 * @param bool $ignoredirs whether or not ignore directories
341 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
344 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
345 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
347 if ($itemid !== false) {
348 $params['itemid'] = $itemid;
349 $where .= " AND itemid = :itemid";
355 WHERE $where AND filename <> '.'";
359 WHERE $where AND (filename <> '.' OR filepath <> '/')";
362 return !$DB->record_exists_sql($sql, $params);
366 * Returns all files belonging to given repository
368 * @param int $repositoryid
369 * @param string $sort
371 public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
373 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
375 LEFT JOIN {files_reference} r
376 ON f.referencefileid = r.id
377 WHERE r.repositoryid = ?
381 $filerecords = $DB->get_records_sql($sql, array($repositoryid));
382 foreach ($filerecords as $filerecord) {
383 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
389 * Returns all area files (optionally limited by itemid)
391 * @param int $contextid context ID
392 * @param string $component component
393 * @param string $filearea file area
394 * @param int $itemid item ID or all files if not specified
395 * @param string $sort sort fields
396 * @param bool $includedirs whether or not include directories
397 * @return array of stored_files indexed by pathanmehash
399 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
402 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
403 if ($itemid !== false) {
404 $itemidsql = ' AND f.itemid = :itemid ';
405 $conditions['itemid'] = $itemid;
410 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
412 LEFT JOIN {files_reference} r
413 ON f.referencefileid = r.id
414 WHERE f.contextid = :contextid
415 AND f.component = :component
416 AND f.filearea = :filearea
421 $filerecords = $DB->get_records_sql($sql, $conditions);
422 foreach ($filerecords as $filerecord) {
423 if (!$includedirs and $filerecord->filename === '.') {
426 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
432 * Returns array based tree structure of area files
434 * @param int $contextid context ID
435 * @param string $component component
436 * @param string $filearea file area
437 * @param int $itemid item ID
438 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
440 public function get_area_tree($contextid, $component, $filearea, $itemid) {
441 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
442 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
443 // first create directory structure
444 foreach ($files as $hash=>$dir) {
445 if (!$dir->is_directory()) {
448 unset($files[$hash]);
449 if ($dir->get_filepath() === '/') {
450 $result['dirfile'] = $dir;
453 $parts = explode('/', trim($dir->get_filepath(),'/'));
455 foreach ($parts as $part) {
459 if (!isset($pointer['subdirs'][$part])) {
460 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
462 $pointer =& $pointer['subdirs'][$part];
464 $pointer['dirfile'] = $dir;
467 foreach ($files as $hash=>$file) {
468 $parts = explode('/', trim($file->get_filepath(),'/'));
470 foreach ($parts as $part) {
474 $pointer =& $pointer['subdirs'][$part];
476 $pointer['files'][$file->get_filename()] = $file;
483 * Returns all files and optionally directories
485 * @param int $contextid context ID
486 * @param string $component component
487 * @param string $filearea file area
488 * @param int $itemid item ID
489 * @param int $filepath directory path
490 * @param bool $recursive include all subdirectories
491 * @param bool $includedirs include files and directories
492 * @param string $sort sort fields
493 * @return array of stored_files indexed by pathanmehash
495 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
498 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
504 $dirs = $includedirs ? "" : "AND filename <> '.'";
505 $length = textlib::strlen($filepath);
507 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
509 LEFT JOIN {files_reference} r
510 ON f.referencefileid = r.id
511 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
512 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
516 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
520 $filerecords = $DB->get_records_sql($sql, $params);
521 foreach ($filerecords as $filerecord) {
522 if ($filerecord->filename == '.') {
523 $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
525 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
528 $result = array_merge($dirs, $files);
532 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
534 $length = textlib::strlen($filepath);
537 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
539 LEFT JOIN {files_reference} r
540 ON f.referencefileid = r.id
541 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
542 AND f.itemid = :itemid AND f.filename = '.'
543 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
546 $reqlevel = substr_count($filepath, '/') + 1;
547 $filerecords = $DB->get_records_sql($sql, $params);
548 foreach ($filerecords as $filerecord) {
549 if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
552 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
556 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
558 LEFT JOIN {files_reference} r
559 ON f.referencefileid = r.id
560 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
561 AND f.filepath = :filepath AND f.filename <> '.'
564 $filerecords = $DB->get_records_sql($sql, $params);
565 foreach ($filerecords as $filerecord) {
566 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
574 * Delete all area files (optionally limited by itemid).
576 * @param int $contextid context ID
577 * @param string $component component
578 * @param string $filearea file area or all areas in context if not specified
579 * @param int $itemid item ID or all files if not specified
580 * @return bool success
582 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
585 $conditions = array('contextid'=>$contextid);
586 if ($component !== false) {
587 $conditions['component'] = $component;
589 if ($filearea !== false) {
590 $conditions['filearea'] = $filearea;
592 if ($itemid !== false) {
593 $conditions['itemid'] = $itemid;
596 $filerecords = $DB->get_records('files', $conditions);
597 foreach ($filerecords as $filerecord) {
598 $this->get_file_instance($filerecord)->delete();
601 return true; // BC only
605 * Delete all the files from certain areas where itemid is limited by an
606 * arbitrary bit of SQL.
608 * @param int $contextid the id of the context the files belong to. Must be given.
609 * @param string $component the owning component. Must be given.
610 * @param string $filearea the file area name. Must be given.
611 * @param string $itemidstest an SQL fragment that the itemid must match. Used
612 * in the query like WHERE itemid $itemidstest. Must used named parameters,
613 * and may not used named parameters called contextid, component or filearea.
614 * @param array $params any query params used by $itemidstest.
616 public function delete_area_files_select($contextid, $component,
617 $filearea, $itemidstest, array $params = null) {
620 $where = "contextid = :contextid
621 AND component = :component
622 AND filearea = :filearea
623 AND itemid $itemidstest";
624 $params['contextid'] = $contextid;
625 $params['component'] = $component;
626 $params['filearea'] = $filearea;
628 $filerecords = $DB->get_recordset_select('files', $where, $params);
629 foreach ($filerecords as $filerecord) {
630 $this->get_file_instance($filerecord)->delete();
632 $filerecords->close();
636 * Move all the files in a file area from one context to another.
638 * @param int $oldcontextid the context the files are being moved from.
639 * @param int $newcontextid the context the files are being moved to.
640 * @param string $component the plugin that these files belong to.
641 * @param string $filearea the name of the file area.
642 * @param int $itemid file item ID
643 * @return int the number of files moved, for information.
645 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
646 // Note, this code is based on some code that Petr wrote in
647 // forum_move_attachments in mod/forum/lib.php. I moved it here because
648 // I needed it in the question code too.
651 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
652 foreach ($oldfiles as $oldfile) {
653 $filerecord = new stdClass();
654 $filerecord->contextid = $newcontextid;
655 $this->create_file_from_storedfile($filerecord, $oldfile);
660 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
667 * Recursively creates directory.
669 * @param int $contextid context ID
670 * @param string $component component
671 * @param string $filearea file area
672 * @param int $itemid item ID
673 * @param string $filepath file path
674 * @param int $userid the user ID
675 * @return bool success
677 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
680 // validate all parameters, we do not want any rubbish stored in database, right?
681 if (!is_number($contextid) or $contextid < 1) {
682 throw new file_exception('storedfileproblem', 'Invalid contextid');
685 $component = clean_param($component, PARAM_COMPONENT);
686 if (empty($component)) {
687 throw new file_exception('storedfileproblem', 'Invalid component');
690 $filearea = clean_param($filearea, PARAM_AREA);
691 if (empty($filearea)) {
692 throw new file_exception('storedfileproblem', 'Invalid filearea');
695 if (!is_number($itemid) or $itemid < 0) {
696 throw new file_exception('storedfileproblem', 'Invalid itemid');
699 $filepath = clean_param($filepath, PARAM_PATH);
700 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
701 // path must start and end with '/'
702 throw new file_exception('storedfileproblem', 'Invalid file path');
705 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
707 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
711 static $contenthash = null;
713 $this->add_string_to_pool('');
714 $contenthash = sha1('');
719 $dir_record = new stdClass();
720 $dir_record->contextid = $contextid;
721 $dir_record->component = $component;
722 $dir_record->filearea = $filearea;
723 $dir_record->itemid = $itemid;
724 $dir_record->filepath = $filepath;
725 $dir_record->filename = '.';
726 $dir_record->contenthash = $contenthash;
727 $dir_record->filesize = 0;
729 $dir_record->timecreated = $now;
730 $dir_record->timemodified = $now;
731 $dir_record->mimetype = null;
732 $dir_record->userid = $userid;
734 $dir_record->pathnamehash = $pathnamehash;
736 $DB->insert_record('files', $dir_record);
737 $dir_info = $this->get_file_by_hash($pathnamehash);
739 if ($filepath !== '/') {
740 //recurse to parent dirs
741 $filepath = trim($filepath, '/');
742 $filepath = explode('/', $filepath);
743 array_pop($filepath);
744 $filepath = implode('/', $filepath);
745 $filepath = ($filepath === '') ? '/' : "/$filepath/";
746 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
753 * Add new local file based on existing local file.
755 * @param stdClass|array $filerecord object or array describing changes
756 * @param stored_file|int $fileorid id or stored_file instance of the existing local file
757 * @return stored_file instance of newly created file
759 public function create_file_from_storedfile($filerecord, $fileorid) {
762 if ($fileorid instanceof stored_file) {
763 $fid = $fileorid->get_id();
768 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
770 unset($filerecord['id']);
771 unset($filerecord['filesize']);
772 unset($filerecord['contenthash']);
773 unset($filerecord['pathnamehash']);
775 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
777 LEFT JOIN {files_reference} r
778 ON f.referencefileid = r.id
781 if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
782 throw new file_exception('storedfileproblem', 'File does not exist');
785 unset($newrecord->id);
787 foreach ($filerecord as $key => $value) {
788 // validate all parameters, we do not want any rubbish stored in database, right?
789 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
790 throw new file_exception('storedfileproblem', 'Invalid contextid');
793 if ($key == 'component') {
794 $value = clean_param($value, PARAM_COMPONENT);
796 throw new file_exception('storedfileproblem', 'Invalid component');
800 if ($key == 'filearea') {
801 $value = clean_param($value, PARAM_AREA);
803 throw new file_exception('storedfileproblem', 'Invalid filearea');
807 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
808 throw new file_exception('storedfileproblem', 'Invalid itemid');
812 if ($key == 'filepath') {
813 $value = clean_param($value, PARAM_PATH);
814 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
815 // path must start and end with '/'
816 throw new file_exception('storedfileproblem', 'Invalid file path');
820 if ($key == 'filename') {
821 $value = clean_param($value, PARAM_FILE);
823 // path must start and end with '/'
824 throw new file_exception('storedfileproblem', 'Invalid file name');
828 if ($key === 'timecreated' or $key === 'timemodified') {
829 if (!is_number($value)) {
830 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
833 //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)
838 if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
839 $value = clean_param($value, PARAM_INT);
842 $newrecord->$key = $value;
845 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
847 if ($newrecord->filename === '.') {
848 // special case - only this function supports directories ;-)
849 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
850 // update the existing directory with the new data
851 $newrecord->id = $directory->get_id();
852 $DB->update_record('files', $newrecord);
853 return $this->get_file_instance($newrecord);
856 if (!empty($newrecord->repositoryid)) {
858 $referencerecord = new stdClass;
859 $referencerecord->repositoryid = $newrecord->repositoryid;
860 $referencerecord->reference = $newrecord->reference;
861 $referencerecord->lastsync = $newrecord->referencelastsync;
862 $referencerecord->lifetime = $newrecord->referencelifetime;
863 $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
864 } catch (dml_exception $e) {
865 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
866 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
868 $newrecord->referencefileid = $referencerecord->id;
872 $newrecord->id = $DB->insert_record('files', $newrecord);
873 } catch (dml_exception $e) {
874 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
875 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
879 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
881 return $this->get_file_instance($newrecord);
885 * Add new local file.
887 * @param stdClass|array $filerecord object or array describing file
888 * @param string $url the URL to the file
889 * @param array $options {@link download_file_content()} options
890 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
891 * @return stored_file
893 public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
895 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
896 $filerecord = (object)$filerecord; // We support arrays too.
898 $headers = isset($options['headers']) ? $options['headers'] : null;
899 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
900 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
901 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
902 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
903 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
904 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
906 if (!isset($filerecord->filename)) {
907 $parts = explode('/', $url);
908 $filename = array_pop($parts);
909 $filerecord->filename = clean_param($filename, PARAM_FILE);
911 $source = !empty($filerecord->source) ? $filerecord->source : $url;
912 $filerecord->source = clean_param($source, PARAM_URL);
915 check_dir_exists($this->tempdir);
916 $tmpfile = tempnam($this->tempdir, 'newfromurl');
917 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
918 if ($content === false) {
919 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
922 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
925 } catch (Exception $e) {
931 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
932 if ($content === false) {
933 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
935 return $this->create_file_from_string($filerecord, $content);
940 * Add new local file.
942 * @param stdClass|array $filerecord object or array describing file
943 * @param string $pathname path to file or content of file
944 * @return stored_file
946 public function create_file_from_pathname($filerecord, $pathname) {
949 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
950 $filerecord = (object)$filerecord; // We support arrays too.
952 // validate all parameters, we do not want any rubbish stored in database, right?
953 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
954 throw new file_exception('storedfileproblem', 'Invalid contextid');
957 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
958 if (empty($filerecord->component)) {
959 throw new file_exception('storedfileproblem', 'Invalid component');
962 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
963 if (empty($filerecord->filearea)) {
964 throw new file_exception('storedfileproblem', 'Invalid filearea');
967 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
968 throw new file_exception('storedfileproblem', 'Invalid itemid');
971 if (!empty($filerecord->sortorder)) {
972 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
973 $filerecord->sortorder = 0;
976 $filerecord->sortorder = 0;
979 $filerecord->referencefileid = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
980 $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
981 $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
983 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
984 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
985 // path must start and end with '/'
986 throw new file_exception('storedfileproblem', 'Invalid file path');
989 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
990 if ($filerecord->filename === '') {
991 // filename must not be empty
992 throw new file_exception('storedfileproblem', 'Invalid file name');
996 if (isset($filerecord->timecreated)) {
997 if (!is_number($filerecord->timecreated)) {
998 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1000 if ($filerecord->timecreated < 0) {
1001 //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)
1002 $filerecord->timecreated = 0;
1005 $filerecord->timecreated = $now;
1008 if (isset($filerecord->timemodified)) {
1009 if (!is_number($filerecord->timemodified)) {
1010 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1012 if ($filerecord->timemodified < 0) {
1013 //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)
1014 $filerecord->timemodified = 0;
1017 $filerecord->timemodified = $now;
1020 $newrecord = new stdClass();
1022 $newrecord->contextid = $filerecord->contextid;
1023 $newrecord->component = $filerecord->component;
1024 $newrecord->filearea = $filerecord->filearea;
1025 $newrecord->itemid = $filerecord->itemid;
1026 $newrecord->filepath = $filerecord->filepath;
1027 $newrecord->filename = $filerecord->filename;
1029 $newrecord->timecreated = $filerecord->timecreated;
1030 $newrecord->timemodified = $filerecord->timemodified;
1031 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname) : $filerecord->mimetype;
1032 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1033 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1034 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1035 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1036 $newrecord->sortorder = $filerecord->sortorder;
1038 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1040 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1043 $newrecord->id = $DB->insert_record('files', $newrecord);
1044 } catch (dml_exception $e) {
1046 $this->deleted_file_cleanup($newrecord->contenthash);
1048 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1049 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1052 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1054 return $this->get_file_instance($newrecord);
1058 * Add new local file.
1060 * @param stdClass|array $filerecord object or array describing file
1061 * @param string $content content of file
1062 * @return stored_file
1064 public function create_file_from_string($filerecord, $content) {
1067 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1068 $filerecord = (object)$filerecord; // We support arrays too.
1070 // validate all parameters, we do not want any rubbish stored in database, right?
1071 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1072 throw new file_exception('storedfileproblem', 'Invalid contextid');
1075 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1076 if (empty($filerecord->component)) {
1077 throw new file_exception('storedfileproblem', 'Invalid component');
1080 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1081 if (empty($filerecord->filearea)) {
1082 throw new file_exception('storedfileproblem', 'Invalid filearea');
1085 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1086 throw new file_exception('storedfileproblem', 'Invalid itemid');
1089 if (!empty($filerecord->sortorder)) {
1090 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1091 $filerecord->sortorder = 0;
1094 $filerecord->sortorder = 0;
1096 $filerecord->referencefileid = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
1097 $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
1098 $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
1100 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1101 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1102 // path must start and end with '/'
1103 throw new file_exception('storedfileproblem', 'Invalid file path');
1106 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1107 if ($filerecord->filename === '') {
1108 // path must start and end with '/'
1109 throw new file_exception('storedfileproblem', 'Invalid file name');
1113 if (isset($filerecord->timecreated)) {
1114 if (!is_number($filerecord->timecreated)) {
1115 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1117 if ($filerecord->timecreated < 0) {
1118 //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)
1119 $filerecord->timecreated = 0;
1122 $filerecord->timecreated = $now;
1125 if (isset($filerecord->timemodified)) {
1126 if (!is_number($filerecord->timemodified)) {
1127 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1129 if ($filerecord->timemodified < 0) {
1130 //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)
1131 $filerecord->timemodified = 0;
1134 $filerecord->timemodified = $now;
1137 $newrecord = new stdClass();
1139 $newrecord->contextid = $filerecord->contextid;
1140 $newrecord->component = $filerecord->component;
1141 $newrecord->filearea = $filerecord->filearea;
1142 $newrecord->itemid = $filerecord->itemid;
1143 $newrecord->filepath = $filerecord->filepath;
1144 $newrecord->filename = $filerecord->filename;
1146 $newrecord->timecreated = $filerecord->timecreated;
1147 $newrecord->timemodified = $filerecord->timemodified;
1148 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1149 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1150 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1151 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
1152 $newrecord->sortorder = $filerecord->sortorder;
1154 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1155 $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1156 // get mimetype by magic bytes
1157 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname) : $filerecord->mimetype;
1159 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1162 $newrecord->id = $DB->insert_record('files', $newrecord);
1163 } catch (dml_exception $e) {
1165 $this->deleted_file_cleanup($newrecord->contenthash);
1167 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1168 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1171 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1173 return $this->get_file_instance($newrecord);
1177 * Create a moodle file from file reference information
1179 * @param stdClass $filerecord
1180 * @param int $repositoryid
1181 * @param string $reference
1182 * @param array $options options for creating external file
1183 * @return stored_file
1185 public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1188 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1189 $filerecord = (object)$filerecord; // We support arrays too.
1191 // validate all parameters, we do not want any rubbish stored in database, right?
1192 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1193 throw new file_exception('storedfileproblem', 'Invalid contextid');
1196 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1197 if (empty($filerecord->component)) {
1198 throw new file_exception('storedfileproblem', 'Invalid component');
1201 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1202 if (empty($filerecord->filearea)) {
1203 throw new file_exception('storedfileproblem', 'Invalid filearea');
1206 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1207 throw new file_exception('storedfileproblem', 'Invalid itemid');
1210 if (!empty($filerecord->sortorder)) {
1211 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1212 $filerecord->sortorder = 0;
1215 $filerecord->sortorder = 0;
1218 $filerecord->referencefileid = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
1219 $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
1220 $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
1221 $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1222 $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1223 $filerecord->source = empty($filerecord->source) ? null : $filerecord->source;
1224 $filerecord->author = empty($filerecord->author) ? null : $filerecord->author;
1225 $filerecord->license = empty($filerecord->license) ? null : $filerecord->license;
1226 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1227 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1228 // Path must start and end with '/'.
1229 throw new file_exception('storedfileproblem', 'Invalid file path');
1232 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1233 if ($filerecord->filename === '') {
1234 // Path must start and end with '/'.
1235 throw new file_exception('storedfileproblem', 'Invalid file name');
1239 if (isset($filerecord->timecreated)) {
1240 if (!is_number($filerecord->timecreated)) {
1241 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1243 if ($filerecord->timecreated < 0) {
1244 // 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)
1245 $filerecord->timecreated = 0;
1248 $filerecord->timecreated = $now;
1251 if (isset($filerecord->timemodified)) {
1252 if (!is_number($filerecord->timemodified)) {
1253 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1255 if ($filerecord->timemodified < 0) {
1256 // 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)
1257 $filerecord->timemodified = 0;
1260 $filerecord->timemodified = $now;
1263 $transaction = $DB->start_delegated_transaction();
1265 // Insert file reference record.
1267 $referencerecord = new stdClass;
1268 $referencerecord->repositoryid = $repositoryid;
1269 $referencerecord->reference = $reference;
1270 $referencerecord->lastsync = $filerecord->referencelastsync;
1271 $referencerecord->lifetime = $filerecord->referencelifetime;
1272 $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
1273 } catch (dml_exception $e) {
1277 $filerecord->referencefileid = $referencerecord->id;
1279 // External file doesn't have content in moodle.
1280 // So we create an empty file for it.
1281 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1283 $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1286 $filerecord->id = $DB->insert_record('files', $filerecord);
1287 } catch (dml_exception $e) {
1289 $this->deleted_file_cleanup($filerecord->contenthash);
1291 throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1292 $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1295 $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1297 $transaction->allow_commit();
1299 // Adding repositoryid and reference to file record to create stored_file instance
1300 $filerecord->repositoryid = $repositoryid;
1301 $filerecord->reference = $reference;
1302 return $this->get_file_instance($filerecord);
1306 * Creates new image file from existing.
1308 * @param stdClass|array $filerecord object or array describing new file
1309 * @param int|stored_file $fid file id or stored file object
1310 * @param int $newwidth in pixels
1311 * @param int $newheight in pixels
1312 * @param bool $keepaspectratio whether or not keep aspect ratio
1313 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1314 * @return stored_file
1316 public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1317 if (!function_exists('imagecreatefromstring')) {
1318 //Most likely the GD php extension isn't installed
1319 //image conversion cannot succeed
1320 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1323 if ($fid instanceof stored_file) {
1324 $fid = $fid->get_id();
1327 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1329 if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1330 throw new file_exception('storedfileproblem', 'File does not exist');
1333 if (!$imageinfo = $file->get_imageinfo()) {
1334 throw new file_exception('storedfileproblem', 'File is not an image');
1337 if (!isset($filerecord['filename'])) {
1338 $filerecord['filename'] = $file->get_filename();
1341 if (!isset($filerecord['mimetype'])) {
1342 $filerecord['mimetype'] = $imageinfo['mimetype'];
1345 $width = $imageinfo['width'];
1346 $height = $imageinfo['height'];
1347 $mimetype = $imageinfo['mimetype'];
1349 if ($keepaspectratio) {
1350 if (0 >= $newwidth and 0 >= $newheight) {
1351 // no sizes specified
1353 $newheight = $height;
1355 } else if (0 < $newwidth and 0 < $newheight) {
1356 $xheight = ($newwidth*($height/$width));
1357 if ($xheight < $newheight) {
1358 $newheight = (int)$xheight;
1360 $newwidth = (int)($newheight*($width/$height));
1363 } else if (0 < $newwidth) {
1364 $newheight = (int)($newwidth*($height/$width));
1366 } else { //0 < $newheight
1367 $newwidth = (int)($newheight*($width/$height));
1371 if (0 >= $newwidth) {
1374 if (0 >= $newheight) {
1375 $newheight = $height;
1379 $img = imagecreatefromstring($file->get_content());
1380 if ($height != $newheight or $width != $newwidth) {
1381 $newimg = imagecreatetruecolor($newwidth, $newheight);
1382 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1384 throw new file_exception('storedfileproblem', 'Can not resize image');
1391 switch ($filerecord['mimetype']) {
1397 if (is_null($quality)) {
1400 imagejpeg($img, NULL, $quality);
1405 $quality = (int)$quality;
1406 imagepng($img, NULL, $quality, NULL);
1410 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1413 $content = ob_get_contents();
1418 throw new file_exception('storedfileproblem', 'Can not convert image');
1421 return $this->create_file_from_string($filerecord, $content);
1425 * Add file content to sha1 pool.
1427 * @param string $pathname path to file
1428 * @param string $contenthash sha1 hash of content if known (performance only)
1429 * @return array (contenthash, filesize, newfile)
1431 public function add_file_to_pool($pathname, $contenthash = NULL) {
1432 if (!is_readable($pathname)) {
1433 throw new file_exception('storedfilecannotread', '', $pathname);
1436 if (is_null($contenthash)) {
1437 $contenthash = sha1_file($pathname);
1440 $filesize = filesize($pathname);
1442 $hashpath = $this->path_from_hash($contenthash);
1443 $hashfile = "$hashpath/$contenthash";
1445 if (file_exists($hashfile)) {
1446 if (filesize($hashfile) !== $filesize) {
1447 throw new file_pool_content_exception($contenthash);
1452 if (!is_dir($hashpath)) {
1453 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1454 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1459 if (!copy($pathname, $hashfile)) {
1460 throw new file_exception('storedfilecannotread', '', $pathname);
1463 if (filesize($hashfile) !== $filesize) {
1465 throw new file_pool_content_exception($contenthash);
1467 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1471 return array($contenthash, $filesize, $newfile);
1475 * Add string content to sha1 pool.
1477 * @param string $content file content - binary string
1478 * @return array (contenthash, filesize, newfile)
1480 public function add_string_to_pool($content) {
1481 $contenthash = sha1($content);
1482 $filesize = strlen($content); // binary length
1484 $hashpath = $this->path_from_hash($contenthash);
1485 $hashfile = "$hashpath/$contenthash";
1488 if (file_exists($hashfile)) {
1489 if (filesize($hashfile) !== $filesize) {
1490 throw new file_pool_content_exception($contenthash);
1495 if (!is_dir($hashpath)) {
1496 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1497 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1502 file_put_contents($hashfile, $content);
1504 if (filesize($hashfile) !== $filesize) {
1506 throw new file_pool_content_exception($contenthash);
1508 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1511 return array($contenthash, $filesize, $newfile);
1515 * Serve file content using X-Sendfile header.
1516 * Please make sure that all headers are already sent
1517 * and the all access control checks passed.
1519 * @param string $contenthash sah1 hash of the file content to be served
1520 * @return bool success
1522 public function xsendfile($contenthash) {
1524 require_once("$CFG->libdir/xsendfilelib.php");
1526 $hashpath = $this->path_from_hash($contenthash);
1527 return xsendfile("$hashpath/$contenthash");
1533 * @param string $contenthash
1536 public function content_exists($contenthash) {
1537 $dir = $this->path_from_hash($contenthash);
1538 $filepath = $dir . '/' . $contenthash;
1539 return file_exists($filepath);
1543 * Return path to file with given hash.
1545 * NOTE: must not be public, files in pool must not be modified
1547 * @param string $contenthash content hash
1548 * @return string expected file location
1550 protected function path_from_hash($contenthash) {
1551 $l1 = $contenthash[0].$contenthash[1];
1552 $l2 = $contenthash[2].$contenthash[3];
1553 return "$this->filedir/$l1/$l2";
1557 * Return path to file with given hash.
1559 * NOTE: must not be public, files in pool must not be modified
1561 * @param string $contenthash content hash
1562 * @return string expected file location
1564 protected function trash_path_from_hash($contenthash) {
1565 $l1 = $contenthash[0].$contenthash[1];
1566 $l2 = $contenthash[2].$contenthash[3];
1567 return "$this->trashdir/$l1/$l2";
1571 * Tries to recover missing content of file from trash.
1573 * @param stored_file $file stored_file instance
1574 * @return bool success
1576 public function try_content_recovery($file) {
1577 $contenthash = $file->get_contenthash();
1578 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1579 if (!is_readable($trashfile)) {
1580 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1583 // nice, at least alternative trash file in trash root exists
1584 $trashfile = $this->trashdir.'/'.$contenthash;
1586 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1587 //weird, better fail early
1590 $contentdir = $this->path_from_hash($contenthash);
1591 $contentfile = $contentdir.'/'.$contenthash;
1592 if (file_exists($contentfile)) {
1593 //strange, no need to recover anything
1596 if (!is_dir($contentdir)) {
1597 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1601 return rename($trashfile, $contentfile);
1605 * Marks pool file as candidate for deleting.
1607 * DO NOT call directly - reserved for core!!
1609 * @param string $contenthash
1611 public function deleted_file_cleanup($contenthash) {
1614 //Note: this section is critical - in theory file could be reused at the same
1615 // time, if this happens we can still recover the file from trash
1616 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1617 // file content is still used
1620 //move content file to trash
1621 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1622 if (!file_exists($contentfile)) {
1623 //weird, but no problem
1626 $trashpath = $this->trash_path_from_hash($contenthash);
1627 $trashfile = $trashpath.'/'.$contenthash;
1628 if (file_exists($trashfile)) {
1629 // we already have this content in trash, no need to move it there
1630 unlink($contentfile);
1633 if (!is_dir($trashpath)) {
1634 mkdir($trashpath, $this->dirpermissions, true);
1636 rename($contentfile, $trashfile);
1637 chmod($trashfile, $this->filepermissions); // fix permissions if needed
1641 * When user referring to a moodle file, we build the reference field
1643 * @param array $params
1646 public static function pack_reference($params) {
1647 $params = (array)$params;
1648 $reference = array();
1649 $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1650 $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1651 $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT);
1652 $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA);
1653 $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);;
1654 $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE);
1655 return base64_encode(serialize($reference));
1659 * Unpack reference field
1661 * @param string $str
1664 public static function unpack_reference($str) {
1665 return unserialize(base64_decode($str));
1669 * Search references by providing reference content
1671 * @param string $str
1674 public function search_references($str) {
1676 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1678 LEFT JOIN {files_reference} r
1679 ON f.referencefileid = r.id
1680 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1681 AND (f.component <> ? OR f.filearea <> ?)";
1683 $rs = $DB->get_recordset_sql($sql, array($str, 'user', 'draft'));
1685 foreach ($rs as $filerecord) {
1686 $file = $this->get_file_instance($filerecord);
1687 if ($file->is_external_file()) {
1688 $files[$filerecord->pathnamehash] = $file;
1696 * Search references count by providing reference content
1698 * @param string $str
1701 public function search_references_count($str) {
1703 $sql = "SELECT COUNT(f.id)
1705 LEFT JOIN {files_reference} r
1706 ON f.referencefileid = r.id
1707 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1708 AND (f.component <> ? OR f.filearea <> ?)";
1710 $count = $DB->count_records_sql($sql, array($str, 'user', 'draft'));
1715 * Return all files referring to provided stored_file instance
1716 * This won't work for draft files
1718 * @param stored_file $storedfile
1721 public function get_references_by_storedfile($storedfile) {
1725 $params['contextid'] = $storedfile->get_contextid();
1726 $params['component'] = $storedfile->get_component();
1727 $params['filearea'] = $storedfile->get_filearea();
1728 $params['itemid'] = $storedfile->get_itemid();
1729 $params['filename'] = $storedfile->get_filename();
1730 $params['filepath'] = $storedfile->get_filepath();
1731 $params['userid'] = $storedfile->get_userid();
1733 $reference = self::pack_reference($params);
1735 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1737 LEFT JOIN {files_reference} r
1738 ON f.referencefileid = r.id
1739 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1740 AND (f.component <> ? OR f.filearea <> ?)";
1742 $rs = $DB->get_recordset_sql($sql, array($reference, 'user', 'draft'));
1744 foreach ($rs as $filerecord) {
1745 $file = $this->get_file_instance($filerecord);
1746 if ($file->is_external_file()) {
1747 $files[$filerecord->pathnamehash] = $file;
1755 * Return the count files referring to provided stored_file instance
1756 * This won't work for draft files
1758 * @param stored_file $storedfile
1761 public function get_references_count_by_storedfile($storedfile) {
1765 $params['contextid'] = $storedfile->get_contextid();
1766 $params['component'] = $storedfile->get_component();
1767 $params['filearea'] = $storedfile->get_filearea();
1768 $params['itemid'] = $storedfile->get_itemid();
1769 $params['filename'] = $storedfile->get_filename();
1770 $params['filepath'] = $storedfile->get_filepath();
1771 $params['userid'] = $storedfile->get_userid();
1773 $reference = self::pack_reference($params);
1775 $sql = "SELECT COUNT(f.id)
1777 LEFT JOIN {files_reference} r
1778 ON f.referencefileid = r.id
1779 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1780 AND (f.component <> ? OR f.filearea <> ?)";
1782 $count = $DB->count_records_sql($sql, array($reference, 'user', 'draft'));
1787 * Convert file alias to local file
1789 * @param stored_file $storedfile a stored_file instances
1790 * @return stored_file stored_file
1792 public function import_external_file($storedfile) {
1794 require_once($CFG->dirroot.'/repository/lib.php');
1795 // sync external file
1796 repository::sync_external_file($storedfile);
1797 // Remove file references
1798 $storedfile->delete_reference();
1803 * Return mimetype by given file pathname
1805 * If file has a known extension, we return the mimetype based on extension.
1806 * Otherwise (when possible) we try to get the mimetype from file contents.
1808 * @param string $pathname
1811 public static function mimetype($pathname) {
1812 $type = mimeinfo('type', $pathname);
1813 if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1814 $finfo = new finfo(FILEINFO_MIME_TYPE);
1815 $type = mimeinfo_from_type('type', $finfo->file($pathname));
1823 public function cron() {
1826 // find out all stale draft areas (older than 4 days) and purge them
1827 // those are identified by time stamp of the /. root dir
1828 mtrace('Deleting old draft files... ', '');
1829 $old = time() - 60*60*24*4;
1832 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1833 AND timecreated < :old";
1834 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1835 foreach ($rs as $dir) {
1836 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1841 // remove orphaned preview files (that is files in the core preview filearea without
1842 // the existing original file)
1843 mtrace('Deleting orphaned preview files... ', '');
1846 LEFT JOIN {files} o ON (p.filename = o.contenthash)
1847 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
1849 $syscontext = context_system::instance();
1850 $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
1851 foreach ($rs as $orphan) {
1852 $file = $this->get_file_instance($orphan);
1853 if (!$file->is_directory()) {
1860 // remove trash pool files once a day
1861 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1862 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1863 require_once($CFG->libdir.'/filelib.php');
1864 // Delete files that are associated with a context that no longer exists.
1865 mtrace('Cleaning up files from deleted contexts... ', '');
1866 $sql = "SELECT DISTINCT f.contextid
1868 LEFT OUTER JOIN {context} c ON f.contextid = c.id
1869 WHERE c.id IS NULL";
1870 $rs = $DB->get_recordset_sql($sql);
1872 $fs = get_file_storage();
1873 foreach ($rs as $ctx) {
1874 $fs->delete_area_files($ctx->contextid);
1880 mtrace('Deleting trash files... ', '');
1881 fulldelete($this->trashdir);
1882 set_config('fileslastcleanup', time());
1888 * Get the sql formated fields for a file instance to be created from a
1889 * {files} and {files_refernece} join.
1891 * @param string $filesprefix the table prefix for the {files} table
1892 * @param string $filesreferenceprefix the table prefix for the {files_reference} table
1893 * @return string the sql to go after a SELECT
1895 private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
1896 // Note, these fieldnames MUST NOT overlap between the two tables,
1897 // else problems like MDL-33172 occur.
1898 $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
1899 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
1900 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid',
1901 'referencelastsync', 'referencelifetime');
1903 $referencefields = array('repositoryid', 'reference');
1905 // id is specifically named to prevent overlaping between the two tables.
1907 $fields[] = $filesprefix.'.id AS id';
1908 foreach ($filefields as $field) {
1909 $fields[] = "{$filesprefix}.{$field}";
1912 foreach ($referencefields as $field) {
1913 $fields[] = "{$filesreferenceprefix}.{$field}";
1916 return implode(', ', $fields);