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 * Returns location of filedir (file pool).
95 * Do not use, this method is intended for stored_file instances only!!!
97 * @return string pathname
99 public function get_filedir() {
100 return $this->filedir;
104 * Calculates sha1 hash of unique full path name information.
106 * This hash is a unique file identifier - it is used to improve
107 * performance and overcome db index size limits.
109 * @param int $contextid
110 * @param string $component
111 * @param string $filearea
113 * @param string $filepath
114 * @param string $filename
115 * @return string sha1 hash
117 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
118 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
122 * Does this file exist?
124 * @param int $contextid
125 * @param string $component
126 * @param string $filearea
128 * @param string $filepath
129 * @param string $filename
132 public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
133 $filepath = clean_param($filepath, PARAM_PATH);
134 $filename = clean_param($filename, PARAM_FILE);
136 if ($filename === '') {
140 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
141 return $this->file_exists_by_hash($pathnamehash);
145 * Does this file exist?
147 * @param string $pathnamehash
150 public function file_exists_by_hash($pathnamehash) {
153 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
157 * Fetch file using local file id.
159 * Please do not rely on file ids, it is usually easier to use
160 * pathname hashes instead.
163 * @return stored_file instance if exists, false if not
165 public function get_file_by_id($fileid) {
168 if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
169 return new stored_file($this, $file_record);
176 * Fetch file using local file full pathname hash
178 * @param string $pathnamehash
179 * @return stored_file instance if exists, false if not
181 public function get_file_by_hash($pathnamehash) {
184 if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
185 return new stored_file($this, $file_record);
192 * Fetch locally stored file.
194 * @param int $contextid
195 * @param string $component
196 * @param string $filearea
198 * @param string $filepath
199 * @param string $filename
200 * @return stored_file instance if exists, false if not
202 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
205 $filepath = clean_param($filepath, PARAM_PATH);
206 $filename = clean_param($filename, PARAM_FILE);
208 if ($filename === '') {
212 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
213 return $this->get_file_by_hash($pathnamehash);
217 * Are there any files (or directories)
218 * @param int $contextid
219 * @param string $component
220 * @param string $filearea
221 * @param bool|int $itemid tem id or false if all items
222 * @param bool $ignoredirs
225 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
228 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
229 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
231 if ($itemid !== false) {
232 $params['itemid'] = $itemid;
233 $where .= " AND itemid = :itemid";
239 WHERE $where AND filename <> '.'";
243 WHERE $where AND (filename <> '.' OR filepath <> '/')";
246 return !$DB->record_exists_sql($sql, $params);
250 * Returns all area files (optionally limited by itemid)
252 * @param int $contextid
253 * @param string $component
254 * @param string $filearea
255 * @param int $itemid (all files if not specified)
256 * @param string $sort
257 * @param bool $includedirs
258 * @return array of stored_files indexed by pathanmehash
260 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
263 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
264 if ($itemid !== false) {
265 $conditions['itemid'] = $itemid;
269 $file_records = $DB->get_records('files', $conditions, $sort);
270 foreach ($file_records as $file_record) {
271 if (!$includedirs and $file_record->filename === '.') {
274 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
280 * Returns array based tree structure of area files
282 * @param int $contextid
283 * @param string $component
284 * @param string $filearea
286 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
288 public function get_area_tree($contextid, $component, $filearea, $itemid) {
289 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
290 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, $sort="sortorder, itemid, filepath, filename", true);
291 // first create directory structure
292 foreach ($files as $hash=>$dir) {
293 if (!$dir->is_directory()) {
296 unset($files[$hash]);
297 if ($dir->get_filepath() === '/') {
298 $result['dirfile'] = $dir;
301 $parts = explode('/', trim($dir->get_filepath(),'/'));
303 foreach ($parts as $part) {
307 if (!isset($pointer['subdirs'][$part])) {
308 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
310 $pointer =& $pointer['subdirs'][$part];
312 $pointer['dirfile'] = $dir;
315 foreach ($files as $hash=>$file) {
316 $parts = explode('/', trim($file->get_filepath(),'/'));
318 foreach ($parts as $part) {
322 $pointer =& $pointer['subdirs'][$part];
324 $pointer['files'][$file->get_filename()] = $file;
331 * Returns all files and optionally directories
333 * @param int $contextid
334 * @param string $component
335 * @param string $filearea
337 * @param int $filepath directory path
338 * @param bool $recursive include all subdirectories
339 * @param bool $includedirs include files and directories
340 * @param string $sort
341 * @return array of stored_files indexed by pathanmehash
343 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
346 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
352 $dirs = $includedirs ? "" : "AND filename <> '.'";
353 $length = textlib_get_instance()->strlen($filepath);
357 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
358 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
362 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
366 $file_records = $DB->get_records_sql($sql, $params);
367 foreach ($file_records as $file_record) {
368 if ($file_record->filename == '.') {
369 $dirs[$file_record->pathnamehash] = new stored_file($this, $file_record);
371 $files[$file_record->pathnamehash] = new stored_file($this, $file_record);
374 $result = array_merge($dirs, $files);
378 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
380 $length = textlib_get_instance()->strlen($filepath);
385 WHERE contextid = :contextid AND component = :component AND filearea = :filearea
386 AND itemid = :itemid AND filename = '.'
387 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
390 $reqlevel = substr_count($filepath, '/') + 1;
391 $file_records = $DB->get_records_sql($sql, $params);
392 foreach ($file_records as $file_record) {
393 if (substr_count($file_record->filepath, '/') !== $reqlevel) {
396 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
402 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
403 AND filepath = :filepath AND filename <> '.'
406 $file_records = $DB->get_records_sql($sql, $params);
407 foreach ($file_records as $file_record) {
408 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
416 * Delete all area files (optionally limited by itemid).
418 * @param int $contextid
419 * @param string $component
420 * @param string $filearea (all areas in context if not specified)
421 * @param int $itemid (all files if not specified)
422 * @return bool success
424 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
427 $conditions = array('contextid'=>$contextid);
428 if ($component !== false) {
429 $conditions['component'] = $component;
431 if ($filearea !== false) {
432 $conditions['filearea'] = $filearea;
434 if ($itemid !== false) {
435 $conditions['itemid'] = $itemid;
438 $file_records = $DB->get_records('files', $conditions);
439 foreach ($file_records as $file_record) {
440 $stored_file = new stored_file($this, $file_record);
441 $stored_file->delete();
444 return true; // BC only
448 * Recursively creates directory.
450 * @param int $contextid
451 * @param string $component
452 * @param string $filearea
454 * @param string $filepath
455 * @param string $filename
456 * @return bool success
458 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
461 // validate all parameters, we do not want any rubbish stored in database, right?
462 if (!is_number($contextid) or $contextid < 1) {
463 throw new file_exception('storedfileproblem', 'Invalid contextid');
466 if ($component === '' or $component !== clean_param($component, PARAM_ALPHAEXT)) {
467 throw new file_exception('storedfileproblem', 'Invalid component');
470 if ($filearea === '' or $filearea !== clean_param($filearea, PARAM_ALPHAEXT)) {
471 throw new file_exception('storedfileproblem', 'Invalid filearea');
474 if (!is_number($itemid) or $itemid < 0) {
475 throw new file_exception('storedfileproblem', 'Invalid itemid');
478 $filepath = clean_param($filepath, PARAM_PATH);
479 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
480 // path must start and end with '/'
481 throw new file_exception('storedfileproblem', 'Invalid file path');
484 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
486 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
490 static $contenthash = null;
492 $this->add_string_to_pool('');
493 $contenthash = sha1('');
498 $dir_record = new object();
499 $dir_record->contextid = $contextid;
500 $dir_record->component = $component;
501 $dir_record->filearea = $filearea;
502 $dir_record->itemid = $itemid;
503 $dir_record->filepath = $filepath;
504 $dir_record->filename = '.';
505 $dir_record->contenthash = $contenthash;
506 $dir_record->filesize = 0;
508 $dir_record->timecreated = $now;
509 $dir_record->timemodified = $now;
510 $dir_record->mimetype = null;
511 $dir_record->userid = $userid;
513 $dir_record->pathnamehash = $pathnamehash;
515 $DB->insert_record('files', $dir_record);
516 $dir_info = $this->get_file_by_hash($pathnamehash);
518 if ($filepath !== '/') {
519 //recurse to parent dirs
520 $filepath = trim($filepath, '/');
521 $filepath = explode('/', $filepath);
522 array_pop($filepath);
523 $filepath = implode('/', $filepath);
524 $filepath = ($filepath === '') ? '/' : "/$filepath/";
525 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
532 * Add new local file based on existing local file.
534 * @param mixed $file_record object or array describing changes
535 * @param mixed $fileorid id or stored_file instance of the existing local file
536 * @return stored_file instance of newly created file
538 public function create_file_from_storedfile($file_record, $fileorid) {
541 if ($fileorid instanceof stored_file) {
542 $fid = $fileorid->get_id();
547 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
549 unset($file_record['id']);
550 unset($file_record['filesize']);
551 unset($file_record['contenthash']);
552 unset($file_record['pathnamehash']);
556 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
557 throw new file_exception('storedfileproblem', 'File does not exist');
560 unset($newrecord->id);
562 foreach ($file_record as $key=>$value) {
563 // validate all parameters, we do not want any rubbish stored in database, right?
564 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
565 throw new file_exception('storedfileproblem', 'Invalid contextid');
568 if ($key == 'component') {
569 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
570 throw new file_exception('storedfileproblem', 'Invalid component');
574 if ($key == 'filearea') {
575 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
576 throw new file_exception('storedfileproblem', 'Invalid filearea');
580 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
581 throw new file_exception('storedfileproblem', 'Invalid itemid');
585 if ($key == 'filepath') {
586 $value = clean_param($value, PARAM_PATH);
587 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
588 // path must start and end with '/'
589 throw new file_exception('storedfileproblem', 'Invalid file path');
593 if ($key == 'filename') {
594 $value = clean_param($value, PARAM_FILE);
596 // path must start and end with '/'
597 throw new file_exception('storedfileproblem', 'Invalid file name');
601 $newrecord->$key = $value;
604 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
606 if ($newrecord->filename === '.') {
607 // special case - only this function supports directories ;-)
608 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
609 // update the existing directory with the new data
610 $newrecord->id = $directory->get_id();
611 $DB->update_record('files', $newrecord);
612 return new stored_file($this, $newrecord);
616 $newrecord->id = $DB->insert_record('files', $newrecord);
617 } catch (database_exception $e) {
618 $newrecord->id = false;
621 if (!$newrecord->id) {
622 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
623 $newrecord->filepath, $newrecord->filename);
626 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
628 return new stored_file($this, $newrecord);
632 * Add new local file.
634 * @param mixed $file_record object or array describing file
635 * @param string $path path to file or content of file
636 * @param array $options @see download_file_content() options
637 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
638 * @return stored_file instance
640 public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
642 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
643 $file_record = (object)$file_record; // we support arrays too
645 $headers = isset($options['headers']) ? $options['headers'] : null;
646 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
647 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
648 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
649 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
650 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
652 if (!isset($file_record->filename)) {
653 $parts = explode('/', $url);
654 $filename = array_pop($parts);
655 $file_record->filename = clean_param($filename, PARAM_FILE);
657 $source = !empty($file_record->source) ? $file_record->source : $url;
658 $file_record->source = clean_param($source, PARAM_URL);
661 check_dir_exists($this->tempdir);
662 $tmpfile = tempnam($this->tempdir, 'newfromurl');
663 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile);
664 if ($content === false) {
665 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
668 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
671 } catch (Exception $e) {
677 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify);
678 if ($content === false) {
679 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
681 return $this->create_file_from_string($file_record, $content);
686 * Add new local file.
688 * @param mixed $file_record object or array describing file
689 * @param string $path path to file or content of file
690 * @return stored_file instance
692 public function create_file_from_pathname($file_record, $pathname) {
695 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
696 $file_record = (object)$file_record; // we support arrays too
698 // validate all parameters, we do not want any rubbish stored in database, right?
699 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
700 throw new file_exception('storedfileproblem', 'Invalid contextid');
703 if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
704 throw new file_exception('storedfileproblem', 'Invalid component');
707 if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
708 throw new file_exception('storedfileproblem', 'Invalid filearea');
711 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
712 throw new file_exception('storedfileproblem', 'Invalid itemid');
715 if (!empty($file_record->sortorder)) {
716 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
717 $file_record->sortorder = 0;
720 $file_record->sortorder = 0;
723 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
724 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
725 // path must start and end with '/'
726 throw new file_exception('storedfileproblem', 'Invalid file path');
729 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
730 if ($file_record->filename === '') {
731 // filename must not be empty
732 throw new file_exception('storedfileproblem', 'Invalid file name');
737 $newrecord = new object();
739 $newrecord->contextid = $file_record->contextid;
740 $newrecord->component = $file_record->component;
741 $newrecord->filearea = $file_record->filearea;
742 $newrecord->itemid = $file_record->itemid;
743 $newrecord->filepath = $file_record->filepath;
744 $newrecord->filename = $file_record->filename;
746 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
747 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
748 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
749 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
750 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
751 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
752 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
753 $newrecord->sortorder = $file_record->sortorder;
755 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
757 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
760 $newrecord->id = $DB->insert_record('files', $newrecord);
761 } catch (database_exception $e) {
762 $newrecord->id = false;
765 if (!$newrecord->id) {
767 $this->deleted_file_cleanup($newrecord->contenthash);
769 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
770 $newrecord->filepath, $newrecord->filename);
773 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
775 return new stored_file($this, $newrecord);
779 * Add new local file.
781 * @param mixed $file_record object or array describing file
782 * @param string $content content of file
783 * @return stored_file instance
785 public function create_file_from_string($file_record, $content) {
788 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
789 $file_record = (object)$file_record; // we support arrays too
791 // validate all parameters, we do not want any rubbish stored in database, right?
792 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
793 throw new file_exception('storedfileproblem', 'Invalid contextid');
796 if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
797 throw new file_exception('storedfileproblem', 'Invalid component');
800 if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
801 throw new file_exception('storedfileproblem', 'Invalid filearea');
804 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
805 throw new file_exception('storedfileproblem', 'Invalid itemid');
808 if (!empty($file_record->sortorder)) {
809 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
810 $file_record->sortorder = 0;
813 $file_record->sortorder = 0;
816 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
817 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
818 // path must start and end with '/'
819 throw new file_exception('storedfileproblem', 'Invalid file path');
822 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
823 if ($file_record->filename === '') {
824 // path must start and end with '/'
825 throw new file_exception('storedfileproblem', 'Invalid file name');
830 $newrecord = new object();
832 $newrecord->contextid = $file_record->contextid;
833 $newrecord->component = $file_record->component;
834 $newrecord->filearea = $file_record->filearea;
835 $newrecord->itemid = $file_record->itemid;
836 $newrecord->filepath = $file_record->filepath;
837 $newrecord->filename = $file_record->filename;
839 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
840 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
841 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
842 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
843 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
844 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
845 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
846 $newrecord->sortorder = $file_record->sortorder;
848 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
850 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
853 $newrecord->id = $DB->insert_record('files', $newrecord);
854 } catch (database_exception $e) {
855 $newrecord->id = false;
858 if (!$newrecord->id) {
860 $this->deleted_file_cleanup($newrecord->contenthash);
862 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
863 $newrecord->filepath, $newrecord->filename);
866 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
868 return new stored_file($this, $newrecord);
872 * Creates new image file from existing.
874 * @param mixed $file_record object or array describing new file
875 * @param mixed file id or stored file object
876 * @param int $newwidth in pixels
877 * @param int $newheight in pixels
878 * @param bool $keepaspectratio
879 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
880 * @return stored_file instance
882 public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
885 if ($fid instanceof stored_file) {
886 $fid = $fid->get_id();
889 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
891 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
892 throw new file_exception('storedfileproblem', 'File does not exist');
895 if (!$imageinfo = $file->get_imageinfo()) {
896 throw new file_exception('storedfileproblem', 'File is not an image');
899 if (!isset($file_record['filename'])) {
900 $file_record['filename'] == $file->get_filename();
903 if (!isset($file_record['mimetype'])) {
904 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
907 $width = $imageinfo['width'];
908 $height = $imageinfo['height'];
909 $mimetype = $imageinfo['mimetype'];
911 if ($keepaspectratio) {
912 if (0 >= $newwidth and 0 >= $newheight) {
913 // no sizes specified
915 $newheight = $height;
917 } else if (0 < $newwidth and 0 < $newheight) {
918 $xheight = ($newwidth*($height/$width));
919 if ($xheight < $newheight) {
920 $newheight = (int)$xheight;
922 $newwidth = (int)($newheight*($width/$height));
925 } else if (0 < $newwidth) {
926 $newheight = (int)($newwidth*($height/$width));
928 } else { //0 < $newheight
929 $newwidth = (int)($newheight*($width/$height));
933 if (0 >= $newwidth) {
936 if (0 >= $newheight) {
937 $newheight = $height;
941 $img = imagecreatefromstring($file->get_content());
942 if ($height != $newheight or $width != $newwidth) {
943 $newimg = imagecreatetruecolor($newwidth, $newheight);
944 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
946 throw new file_exception('storedfileproblem', 'Can not resize image');
953 switch ($file_record['mimetype']) {
959 if (is_null($quality)) {
962 imagejpeg($img, NULL, $quality);
967 $quality = (int)$quality;
968 imagepng($img, NULL, $quality, NULL);
972 throw new file_exception('storedfileproblem', 'Unsupported mime type');
975 $content = ob_get_contents();
980 throw new file_exception('storedfileproblem', 'Can not convert image');
983 return $this->create_file_from_string($file_record, $content);
987 * Add file content to sha1 pool.
989 * @param string $pathname path to file
990 * @param string $contenthash sha1 hash of content if known (performance only)
991 * @return array (contenthash, filesize, newfile)
993 public function add_file_to_pool($pathname, $contenthash = NULL) {
994 if (!is_readable($pathname)) {
995 throw new file_exception('storedfilecannotread');
998 if (is_null($contenthash)) {
999 $contenthash = sha1_file($pathname);
1002 $filesize = filesize($pathname);
1004 $hashpath = $this->path_from_hash($contenthash);
1005 $hashfile = "$hashpath/$contenthash";
1007 if (file_exists($hashfile)) {
1008 if (filesize($hashfile) !== $filesize) {
1009 throw new file_pool_content_exception($contenthash);
1014 if (!is_dir($hashpath)) {
1015 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1016 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1021 if (!copy($pathname, $hashfile)) {
1022 throw new file_exception('storedfilecannotread');
1025 if (filesize($hashfile) !== $filesize) {
1027 throw new file_pool_content_exception($contenthash);
1029 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1033 return array($contenthash, $filesize, $newfile);
1037 * Add string content to sha1 pool.
1039 * @param string $content file content - binary string
1040 * @return array (contenthash, filesize, newfile)
1042 public function add_string_to_pool($content) {
1043 $contenthash = sha1($content);
1044 $filesize = strlen($content); // binary length
1046 $hashpath = $this->path_from_hash($contenthash);
1047 $hashfile = "$hashpath/$contenthash";
1050 if (file_exists($hashfile)) {
1051 if (filesize($hashfile) !== $filesize) {
1052 throw new file_pool_content_exception($contenthash);
1057 if (!is_dir($hashpath)) {
1058 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1059 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1064 file_put_contents($hashfile, $content);
1066 if (filesize($hashfile) !== $filesize) {
1068 throw new file_pool_content_exception($contenthash);
1070 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1073 return array($contenthash, $filesize, $newfile);
1077 * Return path to file with given hash.
1079 * NOTE: must not be public, files in pool must not be modified
1081 * @param string $contenthash
1082 * @return string expected file location
1084 protected function path_from_hash($contenthash) {
1085 $l1 = $contenthash[0].$contenthash[1];
1086 $l2 = $contenthash[2].$contenthash[3];
1087 return "$this->filedir/$l1/$l2";
1091 * Return path to file with given hash.
1093 * NOTE: must not be public, files in pool must not be modified
1095 * @param string $contenthash
1096 * @return string expected file location
1098 protected function trash_path_from_hash($contenthash) {
1099 $l1 = $contenthash[0].$contenthash[1];
1100 $l2 = $contenthash[2].$contenthash[3];
1101 return "$this->trashdir/$l1/$l2";
1105 * Tries to recover missing content of file from trash.
1107 * @param object $file_record
1108 * @return bool success
1110 public function try_content_recovery($file) {
1111 $contenthash = $file->get_contenthash();
1112 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1113 if (!is_readable($trashfile)) {
1114 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1117 // nice, at least alternative trash file in trash root exists
1118 $trashfile = $this->trashdir.'/'.$contenthash;
1120 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1121 //weird, better fail early
1124 $contentdir = $this->path_from_hash($contenthash);
1125 $contentfile = $contentdir.'/'.$contenthash;
1126 if (file_exists($contentfile)) {
1127 //strange, no need to recover anything
1130 if (!is_dir($contentdir)) {
1131 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1135 return rename($trashfile, $contentfile);
1139 * Marks pool file as candidate for deleting.
1141 * DO NOT call directly - reserved for core!!
1143 * @param string $contenthash
1146 public function deleted_file_cleanup($contenthash) {
1149 //Note: this section is critical - in theory file could be reused at the same
1150 // time, if this happens we can still recover the file from trash
1151 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1152 // file content is still used
1155 //move content file to trash
1156 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1157 if (!file_exists($contentfile)) {
1158 //weird, but no problem
1161 $trashpath = $this->trash_path_from_hash($contenthash);
1162 $trashfile = $trashpath.'/'.$contenthash;
1163 if (file_exists($trashfile)) {
1164 // we already have this content in trash, no need to move it there
1165 unlink($contentfile);
1168 if (!is_dir($trashpath)) {
1169 mkdir($trashpath, $this->dirpermissions, true);
1171 rename($contentfile, $trashfile);
1172 chmod($trashfile, $this->filepermissions); // fix permissions if needed
1180 public function cron() {
1183 // find out all stale draft areas (older than 4 days) and purge them
1184 // those are identified by time stamp of the /. root dir
1185 mtrace('Deleting old draft files... ', '');
1186 $old = time() - 60*60*24*4;
1189 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1190 AND timecreated < :old";
1191 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1192 foreach ($rs as $dir) {
1193 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1196 // remove trash pool files once a day
1197 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1198 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1199 require_once($CFG->libdir.'/filelib.php');
1200 // Delete files that are associated with a context that no longer exists.
1201 mtrace('Cleaning up files from deleted contexts... ', '');
1202 $sql = "SELECT DISTINCT f.contextid
1204 LEFT OUTER JOIN {context} c ON f.contextid = c.id
1205 WHERE c.id IS NULL";
1206 if ($rs = $DB->get_recordset_sql($sql)) {
1207 $fs = get_file_storage();
1208 foreach ($rs as $ctx) {
1209 $fs->delete_area_files($ctx->contextid);
1214 mtrace('Deleting trash files... ', '');
1215 fulldelete($this->trashdir);
1216 set_config('fileslastcleanup', time());