3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
20 * Core file storage class definition.
23 * @subpackage filestorage
24 * @copyright 2008 Petr Skoda {@link http://skodak.org}
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
30 require_once("$CFG->libdir/filestorage/stored_file.php");
33 * File storage class used for low level access to stored files.
35 * Only owner of file area may use this class to access own files,
36 * for example only code in mod/assignment/* may access assignment
37 * attachments. When some other part of moodle needs to access
38 * files of modules it has to use file_browser class instead or there
39 * 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 @see 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
99 * @param string $component
100 * @param string $filearea
102 * @param string $filepath
103 * @param string $filename
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
114 * @param string $component
115 * @param string $filearea
117 * @param string $filepath
118 * @param string $filename
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 * Does this file exist?
136 * @param string $pathnamehash
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 $file_record record from the files table
149 * @return stored_file instance of file abstraction class
151 public function get_file_instance(stdClass $file_record) {
152 return new stored_file($this, $file_record, $this->filedir);
156 * Fetch file using local file id.
158 * Please do not rely on file ids, it is usually easier to use
159 * pathname hashes instead.
162 * @return stored_file instance if exists, false if not
164 public function get_file_by_id($fileid) {
167 if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
168 return $this->get_file_instance($file_record);
175 * Fetch file using local file full pathname hash
177 * @param string $pathnamehash
178 * @return stored_file instance if exists, false if not
180 public function get_file_by_hash($pathnamehash) {
183 if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
184 return $this->get_file_instance($file_record);
191 * Fetch locally stored file.
193 * @param int $contextid
194 * @param string $component
195 * @param string $filearea
197 * @param string $filepath
198 * @param string $filename
199 * @return stored_file instance if exists, false if not
201 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
202 $filepath = clean_param($filepath, PARAM_PATH);
203 $filename = clean_param($filename, PARAM_FILE);
205 if ($filename === '') {
209 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
210 return $this->get_file_by_hash($pathnamehash);
214 * Are there any files (or directories)
215 * @param int $contextid
216 * @param string $component
217 * @param string $filearea
218 * @param bool|int $itemid tem id or false if all items
219 * @param bool $ignoredirs
222 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
225 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
226 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
228 if ($itemid !== false) {
229 $params['itemid'] = $itemid;
230 $where .= " AND itemid = :itemid";
236 WHERE $where AND filename <> '.'";
240 WHERE $where AND (filename <> '.' OR filepath <> '/')";
243 return !$DB->record_exists_sql($sql, $params);
247 * Returns all area files (optionally limited by itemid)
249 * @param int $contextid
250 * @param string $component
251 * @param string $filearea
252 * @param int $itemid (all files if not specified)
253 * @param string $sort
254 * @param bool $includedirs
255 * @return array of stored_files indexed by pathanmehash
257 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
260 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
261 if ($itemid !== false) {
262 $conditions['itemid'] = $itemid;
266 $file_records = $DB->get_records('files', $conditions, $sort);
267 foreach ($file_records as $file_record) {
268 if (!$includedirs and $file_record->filename === '.') {
271 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
277 * Returns array based tree structure of area files
279 * @param int $contextid
280 * @param string $component
281 * @param string $filearea
283 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
285 public function get_area_tree($contextid, $component, $filearea, $itemid) {
286 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
287 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
288 // first create directory structure
289 foreach ($files as $hash=>$dir) {
290 if (!$dir->is_directory()) {
293 unset($files[$hash]);
294 if ($dir->get_filepath() === '/') {
295 $result['dirfile'] = $dir;
298 $parts = explode('/', trim($dir->get_filepath(),'/'));
300 foreach ($parts as $part) {
304 if (!isset($pointer['subdirs'][$part])) {
305 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
307 $pointer =& $pointer['subdirs'][$part];
309 $pointer['dirfile'] = $dir;
312 foreach ($files as $hash=>$file) {
313 $parts = explode('/', trim($file->get_filepath(),'/'));
315 foreach ($parts as $part) {
319 $pointer =& $pointer['subdirs'][$part];
321 $pointer['files'][$file->get_filename()] = $file;
328 * Returns all files and optionally directories
330 * @param int $contextid
331 * @param string $component
332 * @param string $filearea
334 * @param int $filepath directory path
335 * @param bool $recursive include all subdirectories
336 * @param bool $includedirs include files and directories
337 * @param string $sort
338 * @return array of stored_files indexed by pathanmehash
340 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
343 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
349 $dirs = $includedirs ? "" : "AND filename <> '.'";
350 $length = textlib_get_instance()->strlen($filepath);
354 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
355 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
359 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
363 $file_records = $DB->get_records_sql($sql, $params);
364 foreach ($file_records as $file_record) {
365 if ($file_record->filename == '.') {
366 $dirs[$file_record->pathnamehash] = $this->get_file_instance($file_record);
368 $files[$file_record->pathnamehash] = $this->get_file_instance($file_record);
371 $result = array_merge($dirs, $files);
375 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
377 $length = textlib_get_instance()->strlen($filepath);
382 WHERE contextid = :contextid AND component = :component AND filearea = :filearea
383 AND itemid = :itemid AND filename = '.'
384 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
387 $reqlevel = substr_count($filepath, '/') + 1;
388 $file_records = $DB->get_records_sql($sql, $params);
389 foreach ($file_records as $file_record) {
390 if (substr_count($file_record->filepath, '/') !== $reqlevel) {
393 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
399 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
400 AND filepath = :filepath AND filename <> '.'
403 $file_records = $DB->get_records_sql($sql, $params);
404 foreach ($file_records as $file_record) {
405 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
413 * Delete all area files (optionally limited by itemid).
415 * @param int $contextid
416 * @param string $component
417 * @param string $filearea (all areas in context if not specified)
418 * @param int $itemid (all files if not specified)
419 * @return bool success
421 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
424 $conditions = array('contextid'=>$contextid);
425 if ($component !== false) {
426 $conditions['component'] = $component;
428 if ($filearea !== false) {
429 $conditions['filearea'] = $filearea;
431 if ($itemid !== false) {
432 $conditions['itemid'] = $itemid;
435 $file_records = $DB->get_records('files', $conditions);
436 foreach ($file_records as $file_record) {
437 $stored_file = $this->get_file_instance($file_record);
438 $stored_file->delete();
441 return true; // BC only
445 * Recursively creates directory.
447 * @param int $contextid
448 * @param string $component
449 * @param string $filearea
451 * @param string $filepath
452 * @param string $filename
453 * @return bool success
455 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
458 // validate all parameters, we do not want any rubbish stored in database, right?
459 if (!is_number($contextid) or $contextid < 1) {
460 throw new file_exception('storedfileproblem', 'Invalid contextid');
463 if ($component === '' or $component !== clean_param($component, PARAM_ALPHAEXT)) {
464 throw new file_exception('storedfileproblem', 'Invalid component');
467 if ($filearea === '' or $filearea !== clean_param($filearea, PARAM_ALPHAEXT)) {
468 throw new file_exception('storedfileproblem', 'Invalid filearea');
471 if (!is_number($itemid) or $itemid < 0) {
472 throw new file_exception('storedfileproblem', 'Invalid itemid');
475 $filepath = clean_param($filepath, PARAM_PATH);
476 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
477 // path must start and end with '/'
478 throw new file_exception('storedfileproblem', 'Invalid file path');
481 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
483 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
487 static $contenthash = null;
489 $this->add_string_to_pool('');
490 $contenthash = sha1('');
495 $dir_record = new object();
496 $dir_record->contextid = $contextid;
497 $dir_record->component = $component;
498 $dir_record->filearea = $filearea;
499 $dir_record->itemid = $itemid;
500 $dir_record->filepath = $filepath;
501 $dir_record->filename = '.';
502 $dir_record->contenthash = $contenthash;
503 $dir_record->filesize = 0;
505 $dir_record->timecreated = $now;
506 $dir_record->timemodified = $now;
507 $dir_record->mimetype = null;
508 $dir_record->userid = $userid;
510 $dir_record->pathnamehash = $pathnamehash;
512 $DB->insert_record('files', $dir_record);
513 $dir_info = $this->get_file_by_hash($pathnamehash);
515 if ($filepath !== '/') {
516 //recurse to parent dirs
517 $filepath = trim($filepath, '/');
518 $filepath = explode('/', $filepath);
519 array_pop($filepath);
520 $filepath = implode('/', $filepath);
521 $filepath = ($filepath === '') ? '/' : "/$filepath/";
522 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
529 * Add new local file based on existing local file.
531 * @param mixed $file_record object or array describing changes
532 * @param mixed $fileorid id or stored_file instance of the existing local file
533 * @return stored_file instance of newly created file
535 public function create_file_from_storedfile($file_record, $fileorid) {
538 if ($fileorid instanceof stored_file) {
539 $fid = $fileorid->get_id();
544 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
546 unset($file_record['id']);
547 unset($file_record['filesize']);
548 unset($file_record['contenthash']);
549 unset($file_record['pathnamehash']);
551 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
552 throw new file_exception('storedfileproblem', 'File does not exist');
555 unset($newrecord->id);
557 foreach ($file_record as $key=>$value) {
558 // validate all parameters, we do not want any rubbish stored in database, right?
559 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
560 throw new file_exception('storedfileproblem', 'Invalid contextid');
563 if ($key == 'component') {
564 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
565 throw new file_exception('storedfileproblem', 'Invalid component');
569 if ($key == 'filearea') {
570 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
571 throw new file_exception('storedfileproblem', 'Invalid filearea');
575 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
576 throw new file_exception('storedfileproblem', 'Invalid itemid');
580 if ($key == 'filepath') {
581 $value = clean_param($value, PARAM_PATH);
582 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
583 // path must start and end with '/'
584 throw new file_exception('storedfileproblem', 'Invalid file path');
588 if ($key == 'filename') {
589 $value = clean_param($value, PARAM_FILE);
591 // path must start and end with '/'
592 throw new file_exception('storedfileproblem', 'Invalid file name');
596 $newrecord->$key = $value;
599 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
601 if ($newrecord->filename === '.') {
602 // special case - only this function supports directories ;-)
603 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
604 // update the existing directory with the new data
605 $newrecord->id = $directory->get_id();
606 $DB->update_record('files', $newrecord);
607 return $this->get_file_instance($newrecord);
611 $newrecord->id = $DB->insert_record('files', $newrecord);
612 } catch (dml_exception $e) {
613 $newrecord->id = false;
616 if (!$newrecord->id) {
617 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
618 $newrecord->filepath, $newrecord->filename);
621 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
623 return $this->get_file_instance($newrecord);
627 * Add new local file.
629 * @param mixed $file_record object or array describing file
630 * @param string $path path to file or content of file
631 * @param array $options @see download_file_content() options
632 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
633 * @return stored_file instance
635 public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
637 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
638 $file_record = (object)$file_record; // we support arrays too
640 $headers = isset($options['headers']) ? $options['headers'] : null;
641 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
642 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
643 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
644 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
645 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
647 if (!isset($file_record->filename)) {
648 $parts = explode('/', $url);
649 $filename = array_pop($parts);
650 $file_record->filename = clean_param($filename, PARAM_FILE);
652 $source = !empty($file_record->source) ? $file_record->source : $url;
653 $file_record->source = clean_param($source, PARAM_URL);
656 check_dir_exists($this->tempdir);
657 $tmpfile = tempnam($this->tempdir, 'newfromurl');
658 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile);
659 if ($content === false) {
660 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
663 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
666 } catch (Exception $e) {
672 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify);
673 if ($content === false) {
674 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
676 return $this->create_file_from_string($file_record, $content);
681 * Add new local file.
683 * @param mixed $file_record object or array describing file
684 * @param string $path path to file or content of file
685 * @return stored_file instance
687 public function create_file_from_pathname($file_record, $pathname) {
690 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
691 $file_record = (object)$file_record; // we support arrays too
693 // validate all parameters, we do not want any rubbish stored in database, right?
694 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
695 throw new file_exception('storedfileproblem', 'Invalid contextid');
698 if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
699 throw new file_exception('storedfileproblem', 'Invalid component');
702 if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
703 throw new file_exception('storedfileproblem', 'Invalid filearea');
706 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
707 throw new file_exception('storedfileproblem', 'Invalid itemid');
710 if (!empty($file_record->sortorder)) {
711 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
712 $file_record->sortorder = 0;
715 $file_record->sortorder = 0;
718 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
719 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
720 // path must start and end with '/'
721 throw new file_exception('storedfileproblem', 'Invalid file path');
724 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
725 if ($file_record->filename === '') {
726 // filename must not be empty
727 throw new file_exception('storedfileproblem', 'Invalid file name');
732 $newrecord = new object();
734 $newrecord->contextid = $file_record->contextid;
735 $newrecord->component = $file_record->component;
736 $newrecord->filearea = $file_record->filearea;
737 $newrecord->itemid = $file_record->itemid;
738 $newrecord->filepath = $file_record->filepath;
739 $newrecord->filename = $file_record->filename;
741 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
742 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
743 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
744 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
745 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
746 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
747 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
748 $newrecord->sortorder = $file_record->sortorder;
750 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
752 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
755 $newrecord->id = $DB->insert_record('files', $newrecord);
756 } catch (dml_exception $e) {
757 $newrecord->id = false;
760 if (!$newrecord->id) {
762 $this->deleted_file_cleanup($newrecord->contenthash);
764 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
765 $newrecord->filepath, $newrecord->filename);
768 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
770 return $this->get_file_instance($newrecord);
774 * Add new local file.
776 * @param mixed $file_record object or array describing file
777 * @param string $content content of file
778 * @return stored_file instance
780 public function create_file_from_string($file_record, $content) {
783 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
784 $file_record = (object)$file_record; // we support arrays too
786 // validate all parameters, we do not want any rubbish stored in database, right?
787 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
788 throw new file_exception('storedfileproblem', 'Invalid contextid');
791 if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
792 throw new file_exception('storedfileproblem', 'Invalid component');
795 if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
796 throw new file_exception('storedfileproblem', 'Invalid filearea');
799 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
800 throw new file_exception('storedfileproblem', 'Invalid itemid');
803 if (!empty($file_record->sortorder)) {
804 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
805 $file_record->sortorder = 0;
808 $file_record->sortorder = 0;
811 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
812 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
813 // path must start and end with '/'
814 throw new file_exception('storedfileproblem', 'Invalid file path');
817 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
818 if ($file_record->filename === '') {
819 // path must start and end with '/'
820 throw new file_exception('storedfileproblem', 'Invalid file name');
825 $newrecord = new object();
827 $newrecord->contextid = $file_record->contextid;
828 $newrecord->component = $file_record->component;
829 $newrecord->filearea = $file_record->filearea;
830 $newrecord->itemid = $file_record->itemid;
831 $newrecord->filepath = $file_record->filepath;
832 $newrecord->filename = $file_record->filename;
834 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
835 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
836 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
837 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
838 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
839 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
840 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
841 $newrecord->sortorder = $file_record->sortorder;
843 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
845 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
848 $newrecord->id = $DB->insert_record('files', $newrecord);
849 } catch (dml_exception $e) {
850 $newrecord->id = false;
853 if (!$newrecord->id) {
855 $this->deleted_file_cleanup($newrecord->contenthash);
857 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
858 $newrecord->filepath, $newrecord->filename);
861 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
863 return $this->get_file_instance($newrecord);
867 * Creates new image file from existing.
869 * @param mixed $file_record object or array describing new file
870 * @param mixed file id or stored file object
871 * @param int $newwidth in pixels
872 * @param int $newheight in pixels
873 * @param bool $keepaspectratio
874 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
875 * @return stored_file instance
877 public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
878 if ($fid instanceof stored_file) {
879 $fid = $fid->get_id();
882 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
884 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
885 throw new file_exception('storedfileproblem', 'File does not exist');
888 if (!$imageinfo = $file->get_imageinfo()) {
889 throw new file_exception('storedfileproblem', 'File is not an image');
892 if (!isset($file_record['filename'])) {
893 $file_record['filename'] == $file->get_filename();
896 if (!isset($file_record['mimetype'])) {
897 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
900 $width = $imageinfo['width'];
901 $height = $imageinfo['height'];
902 $mimetype = $imageinfo['mimetype'];
904 if ($keepaspectratio) {
905 if (0 >= $newwidth and 0 >= $newheight) {
906 // no sizes specified
908 $newheight = $height;
910 } else if (0 < $newwidth and 0 < $newheight) {
911 $xheight = ($newwidth*($height/$width));
912 if ($xheight < $newheight) {
913 $newheight = (int)$xheight;
915 $newwidth = (int)($newheight*($width/$height));
918 } else if (0 < $newwidth) {
919 $newheight = (int)($newwidth*($height/$width));
921 } else { //0 < $newheight
922 $newwidth = (int)($newheight*($width/$height));
926 if (0 >= $newwidth) {
929 if (0 >= $newheight) {
930 $newheight = $height;
934 $img = imagecreatefromstring($file->get_content());
935 if ($height != $newheight or $width != $newwidth) {
936 $newimg = imagecreatetruecolor($newwidth, $newheight);
937 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
939 throw new file_exception('storedfileproblem', 'Can not resize image');
946 switch ($file_record['mimetype']) {
952 if (is_null($quality)) {
955 imagejpeg($img, NULL, $quality);
960 $quality = (int)$quality;
961 imagepng($img, NULL, $quality, NULL);
965 throw new file_exception('storedfileproblem', 'Unsupported mime type');
968 $content = ob_get_contents();
973 throw new file_exception('storedfileproblem', 'Can not convert image');
976 return $this->create_file_from_string($file_record, $content);
980 * Add file content to sha1 pool.
982 * @param string $pathname path to file
983 * @param string $contenthash sha1 hash of content if known (performance only)
984 * @return array (contenthash, filesize, newfile)
986 public function add_file_to_pool($pathname, $contenthash = NULL) {
987 if (!is_readable($pathname)) {
988 throw new file_exception('storedfilecannotread');
991 if (is_null($contenthash)) {
992 $contenthash = sha1_file($pathname);
995 $filesize = filesize($pathname);
997 $hashpath = $this->path_from_hash($contenthash);
998 $hashfile = "$hashpath/$contenthash";
1000 if (file_exists($hashfile)) {
1001 if (filesize($hashfile) !== $filesize) {
1002 throw new file_pool_content_exception($contenthash);
1007 if (!is_dir($hashpath)) {
1008 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1009 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1014 if (!copy($pathname, $hashfile)) {
1015 throw new file_exception('storedfilecannotread');
1018 if (filesize($hashfile) !== $filesize) {
1020 throw new file_pool_content_exception($contenthash);
1022 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1026 return array($contenthash, $filesize, $newfile);
1030 * Add string content to sha1 pool.
1032 * @param string $content file content - binary string
1033 * @return array (contenthash, filesize, newfile)
1035 public function add_string_to_pool($content) {
1036 $contenthash = sha1($content);
1037 $filesize = strlen($content); // binary length
1039 $hashpath = $this->path_from_hash($contenthash);
1040 $hashfile = "$hashpath/$contenthash";
1043 if (file_exists($hashfile)) {
1044 if (filesize($hashfile) !== $filesize) {
1045 throw new file_pool_content_exception($contenthash);
1050 if (!is_dir($hashpath)) {
1051 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1052 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1057 file_put_contents($hashfile, $content);
1059 if (filesize($hashfile) !== $filesize) {
1061 throw new file_pool_content_exception($contenthash);
1063 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1066 return array($contenthash, $filesize, $newfile);
1070 * Return path to file with given hash.
1072 * NOTE: must not be public, files in pool must not be modified
1074 * @param string $contenthash
1075 * @return string expected file location
1077 protected function path_from_hash($contenthash) {
1078 $l1 = $contenthash[0].$contenthash[1];
1079 $l2 = $contenthash[2].$contenthash[3];
1080 return "$this->filedir/$l1/$l2";
1084 * Return path to file with given hash.
1086 * NOTE: must not be public, files in pool must not be modified
1088 * @param string $contenthash
1089 * @return string expected file location
1091 protected function trash_path_from_hash($contenthash) {
1092 $l1 = $contenthash[0].$contenthash[1];
1093 $l2 = $contenthash[2].$contenthash[3];
1094 return "$this->trashdir/$l1/$l2";
1098 * Tries to recover missing content of file from trash.
1100 * @param object $file_record
1101 * @return bool success
1103 public function try_content_recovery($file) {
1104 $contenthash = $file->get_contenthash();
1105 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1106 if (!is_readable($trashfile)) {
1107 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1110 // nice, at least alternative trash file in trash root exists
1111 $trashfile = $this->trashdir.'/'.$contenthash;
1113 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1114 //weird, better fail early
1117 $contentdir = $this->path_from_hash($contenthash);
1118 $contentfile = $contentdir.'/'.$contenthash;
1119 if (file_exists($contentfile)) {
1120 //strange, no need to recover anything
1123 if (!is_dir($contentdir)) {
1124 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1128 return rename($trashfile, $contentfile);
1132 * Marks pool file as candidate for deleting.
1134 * DO NOT call directly - reserved for core!!
1136 * @param string $contenthash
1139 public function deleted_file_cleanup($contenthash) {
1142 //Note: this section is critical - in theory file could be reused at the same
1143 // time, if this happens we can still recover the file from trash
1144 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1145 // file content is still used
1148 //move content file to trash
1149 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1150 if (!file_exists($contentfile)) {
1151 //weird, but no problem
1154 $trashpath = $this->trash_path_from_hash($contenthash);
1155 $trashfile = $trashpath.'/'.$contenthash;
1156 if (file_exists($trashfile)) {
1157 // we already have this content in trash, no need to move it there
1158 unlink($contentfile);
1161 if (!is_dir($trashpath)) {
1162 mkdir($trashpath, $this->dirpermissions, true);
1164 rename($contentfile, $trashfile);
1165 chmod($trashfile, $this->filepermissions); // fix permissions if needed
1173 public function cron() {
1176 // find out all stale draft areas (older than 4 days) and purge them
1177 // those are identified by time stamp of the /. root dir
1178 mtrace('Deleting old draft files... ', '');
1179 $old = time() - 60*60*24*4;
1182 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1183 AND timecreated < :old";
1184 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1185 foreach ($rs as $dir) {
1186 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1189 // remove trash pool files once a day
1190 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1191 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1192 require_once($CFG->libdir.'/filelib.php');
1193 // Delete files that are associated with a context that no longer exists.
1194 mtrace('Cleaning up files from deleted contexts... ', '');
1195 $sql = "SELECT DISTINCT f.contextid
1197 LEFT OUTER JOIN {context} c ON f.contextid = c.id
1198 WHERE c.id IS NULL";
1199 if ($rs = $DB->get_recordset_sql($sql)) {
1200 $fs = get_file_storage();
1201 foreach ($rs as $ctx) {
1202 $fs->delete_area_files($ctx->contextid);
1207 mtrace('Deleting trash files... ', '');
1208 fulldelete($this->trashdir);
1209 set_config('fileslastcleanup', time());