e2331903e505cb860d441d64619e296dce2df8df
[moodle.git] / lib / filestorage / file_storage.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
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.
9 //
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.
14 //
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/>.
19 /**
20  * Core file storage class definition.
21  *
22  * @package    core
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
26  */
28 defined('MOODLE_INTERNAL') || die();
30 require_once("$CFG->libdir/filestorage/stored_file.php");
32 /**
33  * File storage class used for low level access to stored files.
34  *
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.
40  *
41  * @copyright 2008 Petr Skoda {@link http://skodak.org}
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  * @since     Moodle 2.0
44  */
45 class file_storage {
46     /** @var string Directory with file contents */
47     private $filedir;
48     /** @var string Contents of deleted files not needed any more */
49     private $trashdir;
50     /** @var string tempdir */
51     private $tempdir;
52     /** @var int Permissions for new directories */
53     private $dirpermissions;
54     /** @var int Permissions for new files */
55     private $filepermissions;
57     /**
58      * Constructor - do not use directly use @see get_file_storage() call instead.
59      *
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
65      */
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
77             }
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.');
82             }
83         }
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
88             }
89         }
90     }
92     /**
93      * Returns location of filedir (file pool).
94      *
95      * Do not use, this method is intended for stored_file instances only!!!
96      *
97      * @return string pathname
98      */
99     public function get_filedir() {
100         return $this->filedir;
101     }
103     /**
104      * Calculates sha1 hash of unique full path name information.
105      *
106      * This hash is a unique file identifier - it is used to improve
107      * performance and overcome db index size limits.
108      *
109      * @param int $contextid
110      * @param string $component
111      * @param string $filearea
112      * @param int $itemid
113      * @param string $filepath
114      * @param string $filename
115      * @return string sha1 hash
116      */
117     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
118         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
119     }
121     /**
122      * Does this file exist?
123      *
124      * @param int $contextid
125      * @param string $component
126      * @param string $filearea
127      * @param int $itemid
128      * @param string $filepath
129      * @param string $filename
130      * @return bool
131      */
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 === '') {
137             $filename = '.';
138         }
140         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
141         return $this->file_exists_by_hash($pathnamehash);
142     }
144     /**
145      * Does this file exist?
146      *
147      * @param string $pathnamehash
148      * @return bool
149      */
150     public function file_exists_by_hash($pathnamehash) {
151         global $DB;
153         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
154     }
156     /**
157      * Fetch file using local file id.
158      *
159      * Please do not rely on file ids, it is usually easier to use
160      * pathname hashes instead.
161      *
162      * @param int $fileid
163      * @return stored_file instance if exists, false if not
164      */
165     public function get_file_by_id($fileid) {
166         global $DB;
168         if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
169             return new stored_file($this, $file_record);
170         } else {
171             return false;
172         }
173     }
175     /**
176      * Fetch file using local file full pathname hash
177      *
178      * @param string $pathnamehash
179      * @return stored_file instance if exists, false if not
180      */
181     public function get_file_by_hash($pathnamehash) {
182         global $DB;
184         if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
185             return new stored_file($this, $file_record);
186         } else {
187             return false;
188         }
189     }
191     /**
192      * Fetch locally stored file.
193      *
194      * @param int $contextid
195      * @param string $component
196      * @param string $filearea
197      * @param int $itemid
198      * @param string $filepath
199      * @param string $filename
200      * @return stored_file instance if exists, false if not
201      */
202     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
203         $filepath = clean_param($filepath, PARAM_PATH);
204         $filename = clean_param($filename, PARAM_FILE);
206         if ($filename === '') {
207             $filename = '.';
208         }
210         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
211         return $this->get_file_by_hash($pathnamehash);
212     }
214     /**
215      * Are there any files (or directories)
216      * @param int $contextid
217      * @param string $component
218      * @param string $filearea
219      * @param bool|int $itemid tem id or false if all items
220      * @param bool $ignoredirs
221      * @return bool empty
222      */
223     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
224         global $DB;
226         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
227         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
229         if ($itemid !== false) {
230             $params['itemid'] = $itemid;
231             $where .= " AND itemid = :itemid";
232         }
234         if ($ignoredirs) {
235             $sql = "SELECT 'x'
236                       FROM {files}
237                      WHERE $where AND filename <> '.'";
238         } else {
239             $sql = "SELECT 'x'
240                       FROM {files}
241                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
242         }
244         return !$DB->record_exists_sql($sql, $params);
245     }
247     /**
248      * Returns all area files (optionally limited by itemid)
249      *
250      * @param int $contextid
251      * @param string $component
252      * @param string $filearea
253      * @param int $itemid (all files if not specified)
254      * @param string $sort
255      * @param bool $includedirs
256      * @return array of stored_files indexed by pathanmehash
257      */
258     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
259         global $DB;
261         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
262         if ($itemid !== false) {
263             $conditions['itemid'] = $itemid;
264         }
266         $result = array();
267         $file_records = $DB->get_records('files', $conditions, $sort);
268         foreach ($file_records as $file_record) {
269             if (!$includedirs and $file_record->filename === '.') {
270                 continue;
271             }
272             $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
273         }
274         return $result;
275     }
277     /**
278      * Returns array based tree structure of area files
279      *
280      * @param int $contextid
281      * @param string $component
282      * @param string $filearea
283      * @param int $itemid
284      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
285      */
286     public function get_area_tree($contextid, $component, $filearea, $itemid) {
287         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
288         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
289         // first create directory structure
290         foreach ($files as $hash=>$dir) {
291             if (!$dir->is_directory()) {
292                 continue;
293             }
294             unset($files[$hash]);
295             if ($dir->get_filepath() === '/') {
296                 $result['dirfile'] = $dir;
297                 continue;
298             }
299             $parts = explode('/', trim($dir->get_filepath(),'/'));
300             $pointer =& $result;
301             foreach ($parts as $part) {
302                 if ($part === '') {
303                     continue;
304                 }
305                 if (!isset($pointer['subdirs'][$part])) {
306                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
307                 }
308                 $pointer =& $pointer['subdirs'][$part];
309             }
310             $pointer['dirfile'] = $dir;
311             unset($pointer);
312         }
313         foreach ($files as $hash=>$file) {
314             $parts = explode('/', trim($file->get_filepath(),'/'));
315             $pointer =& $result;
316             foreach ($parts as $part) {
317                 if ($part === '') {
318                     continue;
319                 }
320                 $pointer =& $pointer['subdirs'][$part];
321             }
322             $pointer['files'][$file->get_filename()] = $file;
323             unset($pointer);
324         }
325         return $result;
326     }
328     /**
329      * Returns all files and optionally directories
330      *
331      * @param int $contextid
332      * @param string $component
333      * @param string $filearea
334      * @param int $itemid
335      * @param int $filepath directory path
336      * @param bool $recursive include all subdirectories
337      * @param bool $includedirs include files and directories
338      * @param string $sort
339      * @return array of stored_files indexed by pathanmehash
340      */
341     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
342         global $DB;
344         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
345             return array();
346         }
348         if ($recursive) {
350             $dirs = $includedirs ? "" : "AND filename <> '.'";
351             $length = textlib_get_instance()->strlen($filepath);
353             $sql = "SELECT *
354                       FROM {files}
355                      WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
356                            AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
357                            AND id <> :dirid
358                            $dirs
359                   ORDER BY $sort";
360             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
362             $files = array();
363             $dirs  = array();
364             $file_records = $DB->get_records_sql($sql, $params);
365             foreach ($file_records as $file_record) {
366                 if ($file_record->filename == '.') {
367                     $dirs[$file_record->pathnamehash] = new stored_file($this, $file_record);
368                 } else {
369                     $files[$file_record->pathnamehash] = new stored_file($this, $file_record);
370                 }
371             }
372             $result = array_merge($dirs, $files);
374         } else {
375             $result = array();
376             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
378             $length = textlib_get_instance()->strlen($filepath);
380             if ($includedirs) {
381                 $sql = "SELECT *
382                           FROM {files}
383                          WHERE contextid = :contextid AND component = :component AND filearea = :filearea
384                                AND itemid = :itemid AND filename = '.'
385                                AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
386                                AND id <> :dirid
387                       ORDER BY $sort";
388                 $reqlevel = substr_count($filepath, '/') + 1;
389                 $file_records = $DB->get_records_sql($sql, $params);
390                 foreach ($file_records as $file_record) {
391                     if (substr_count($file_record->filepath, '/') !== $reqlevel) {
392                         continue;
393                     }
394                     $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
395                 }
396             }
398             $sql = "SELECT *
399                       FROM {files}
400                      WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
401                            AND filepath = :filepath AND filename <> '.'
402                   ORDER BY $sort";
404             $file_records = $DB->get_records_sql($sql, $params);
405             foreach ($file_records as $file_record) {
406                 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
407             }
408         }
410         return $result;
411     }
413     /**
414      * Delete all area files (optionally limited by itemid).
415      *
416      * @param int $contextid
417      * @param string $component
418      * @param string $filearea (all areas in context if not specified)
419      * @param int $itemid (all files if not specified)
420      * @return bool success
421      */
422     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
423         global $DB;
425         $conditions = array('contextid'=>$contextid);
426         if ($component !== false) {
427             $conditions['component'] = $component;
428         }
429         if ($filearea !== false) {
430             $conditions['filearea'] = $filearea;
431         }
432         if ($itemid !== false) {
433             $conditions['itemid'] = $itemid;
434         }
436         $file_records = $DB->get_records('files', $conditions);
437         foreach ($file_records as $file_record) {
438             $stored_file = new stored_file($this, $file_record);
439             $stored_file->delete();
440         }
442         return true; // BC only
443     }
445     /**
446      * Recursively creates directory.
447      *
448      * @param int $contextid
449      * @param string $component
450      * @param string $filearea
451      * @param int $itemid
452      * @param string $filepath
453      * @param string $filename
454      * @return bool success
455      */
456     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
457         global $DB;
459         // validate all parameters, we do not want any rubbish stored in database, right?
460         if (!is_number($contextid) or $contextid < 1) {
461             throw new file_exception('storedfileproblem', 'Invalid contextid');
462         }
464         if ($component === '' or $component !== clean_param($component, PARAM_ALPHAEXT)) {
465             throw new file_exception('storedfileproblem', 'Invalid component');
466         }
468         if ($filearea === '' or $filearea !== clean_param($filearea, PARAM_ALPHAEXT)) {
469             throw new file_exception('storedfileproblem', 'Invalid filearea');
470         }
472         if (!is_number($itemid) or $itemid < 0) {
473             throw new file_exception('storedfileproblem', 'Invalid itemid');
474         }
476         $filepath = clean_param($filepath, PARAM_PATH);
477         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
478             // path must start and end with '/'
479             throw new file_exception('storedfileproblem', 'Invalid file path');
480         }
482         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
484         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
485             return $dir_info;
486         }
488         static $contenthash = null;
489         if (!$contenthash) {
490             $this->add_string_to_pool('');
491             $contenthash = sha1('');
492         }
494         $now = time();
496         $dir_record = new object();
497         $dir_record->contextid = $contextid;
498         $dir_record->component = $component;
499         $dir_record->filearea  = $filearea;
500         $dir_record->itemid    = $itemid;
501         $dir_record->filepath  = $filepath;
502         $dir_record->filename  = '.';
503         $dir_record->contenthash  = $contenthash;
504         $dir_record->filesize  = 0;
506         $dir_record->timecreated  = $now;
507         $dir_record->timemodified = $now;
508         $dir_record->mimetype     = null;
509         $dir_record->userid       = $userid;
511         $dir_record->pathnamehash = $pathnamehash;
513         $DB->insert_record('files', $dir_record);
514         $dir_info = $this->get_file_by_hash($pathnamehash);
516         if ($filepath !== '/') {
517             //recurse to parent dirs
518             $filepath = trim($filepath, '/');
519             $filepath = explode('/', $filepath);
520             array_pop($filepath);
521             $filepath = implode('/', $filepath);
522             $filepath = ($filepath === '') ? '/' : "/$filepath/";
523             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
524         }
526         return $dir_info;
527     }
529     /**
530      * Add new local file based on existing local file.
531      *
532      * @param mixed $file_record object or array describing changes
533      * @param mixed $fileorid id or stored_file instance of the existing local file
534      * @return stored_file instance of newly created file
535      */
536     public function create_file_from_storedfile($file_record, $fileorid) {
537         global $DB;
539         if ($fileorid instanceof stored_file) {
540             $fid = $fileorid->get_id();
541         } else {
542             $fid = $fileorid;
543         }
545         $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
547         unset($file_record['id']);
548         unset($file_record['filesize']);
549         unset($file_record['contenthash']);
550         unset($file_record['pathnamehash']);
552         if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
553             throw new file_exception('storedfileproblem', 'File does not exist');
554         }
556         unset($newrecord->id);
558         foreach ($file_record as $key=>$value) {
559             // validate all parameters, we do not want any rubbish stored in database, right?
560             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
561                 throw new file_exception('storedfileproblem', 'Invalid contextid');
562             }
564             if ($key == 'component') {
565                 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
566                     throw new file_exception('storedfileproblem', 'Invalid component');
567                 }
568             }
570             if ($key == 'filearea') {
571                 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
572                     throw new file_exception('storedfileproblem', 'Invalid filearea');
573                 }
574             }
576             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
577                 throw new file_exception('storedfileproblem', 'Invalid itemid');
578             }
581             if ($key == 'filepath') {
582                 $value = clean_param($value, PARAM_PATH);
583                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
584                     // path must start and end with '/'
585                     throw new file_exception('storedfileproblem', 'Invalid file path');
586                 }
587             }
589             if ($key == 'filename') {
590                 $value = clean_param($value, PARAM_FILE);
591                 if ($value === '') {
592                     // path must start and end with '/'
593                     throw new file_exception('storedfileproblem', 'Invalid file name');
594                 }
595             }
597             $newrecord->$key = $value;
598         }
600         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
602         if ($newrecord->filename === '.') {
603             // special case - only this function supports directories ;-)
604             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
605             // update the existing directory with the new data
606             $newrecord->id = $directory->get_id();
607             $DB->update_record('files', $newrecord);
608             return new stored_file($this, $newrecord);
609         }
611         try {
612             $newrecord->id = $DB->insert_record('files', $newrecord);
613         } catch (dml_exception $e) {
614             $newrecord->id = false;
615         }
617         if (!$newrecord->id) {
618             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
619                                                      $newrecord->filepath, $newrecord->filename);
620         }
622         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
624         return new stored_file($this, $newrecord);
625     }
627     /**
628      * Add new local file.
629      *
630      * @param mixed $file_record object or array describing file
631      * @param string $path path to file or content of file
632      * @param array $options @see download_file_content() options
633      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
634      * @return stored_file instance
635      */
636     public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
638         $file_record = (array)$file_record;  //do not modify the submitted record, this cast unlinks objects
639         $file_record = (object)$file_record; // we support arrays too
641         $headers        = isset($options['headers'])        ? $options['headers'] : null;
642         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
643         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
644         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
645         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
646         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
648         if (!isset($file_record->filename)) {
649             $parts = explode('/', $url);
650             $filename = array_pop($parts);
651             $file_record->filename = clean_param($filename, PARAM_FILE);
652         }
653         $source = !empty($file_record->source) ? $file_record->source : $url;
654         $file_record->source = clean_param($source, PARAM_URL);
656         if ($usetempfile) {
657             check_dir_exists($this->tempdir);
658             $tmpfile = tempnam($this->tempdir, 'newfromurl');
659             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile);
660             if ($content === false) {
661                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
662             }
663             try {
664                 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
665                 @unlink($tmpfile);
666                 return $newfile;
667             } catch (Exception $e) {
668                 @unlink($tmpfile);
669                 throw $e;
670             }
672         } else {
673             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify);
674             if ($content === false) {
675                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
676             }
677             return $this->create_file_from_string($file_record, $content);
678         }
679     }
681     /**
682      * Add new local file.
683      *
684      * @param mixed $file_record object or array describing file
685      * @param string $path path to file or content of file
686      * @return stored_file instance
687      */
688     public function create_file_from_pathname($file_record, $pathname) {
689         global $DB;
691         $file_record = (array)$file_record;  //do not modify the submitted record, this cast unlinks objects
692         $file_record = (object)$file_record; // we support arrays too
694         // validate all parameters, we do not want any rubbish stored in database, right?
695         if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
696             throw new file_exception('storedfileproblem', 'Invalid contextid');
697         }
699         if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
700             throw new file_exception('storedfileproblem', 'Invalid component');
701         }
703         if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
704             throw new file_exception('storedfileproblem', 'Invalid filearea');
705         }
707         if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
708             throw new file_exception('storedfileproblem', 'Invalid itemid');
709         }
711         if (!empty($file_record->sortorder)) {
712             if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
713                 $file_record->sortorder = 0;
714             }
715         } else {
716             $file_record->sortorder = 0;
717         }
719         $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
720         if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
721             // path must start and end with '/'
722             throw new file_exception('storedfileproblem', 'Invalid file path');
723         }
725         $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
726         if ($file_record->filename === '') {
727             // filename must not be empty
728             throw new file_exception('storedfileproblem', 'Invalid file name');
729         }
731         $now = time();
733         $newrecord = new object();
735         $newrecord->contextid = $file_record->contextid;
736         $newrecord->component = $file_record->component;
737         $newrecord->filearea  = $file_record->filearea;
738         $newrecord->itemid    = $file_record->itemid;
739         $newrecord->filepath  = $file_record->filepath;
740         $newrecord->filename  = $file_record->filename;
742         $newrecord->timecreated  = empty($file_record->timecreated) ? $now : $file_record->timecreated;
743         $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
744         $newrecord->mimetype     = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
745         $newrecord->userid       = empty($file_record->userid) ? null : $file_record->userid;
746         $newrecord->source       = empty($file_record->source) ? null : $file_record->source;
747         $newrecord->author       = empty($file_record->author) ? null : $file_record->author;
748         $newrecord->license      = empty($file_record->license) ? null : $file_record->license;
749         $newrecord->sortorder    = $file_record->sortorder;
751         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
753         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
755         try {
756             $newrecord->id = $DB->insert_record('files', $newrecord);
757         } catch (dml_exception $e) {
758             $newrecord->id = false;
759         }
761         if (!$newrecord->id) {
762             if ($newfile) {
763                 $this->deleted_file_cleanup($newrecord->contenthash);
764             }
765             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
766                                                     $newrecord->filepath, $newrecord->filename);
767         }
769         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
771         return new stored_file($this, $newrecord);
772     }
774     /**
775      * Add new local file.
776      *
777      * @param mixed $file_record object or array describing file
778      * @param string $content content of file
779      * @return stored_file instance
780      */
781     public function create_file_from_string($file_record, $content) {
782         global $DB;
784         $file_record = (array)$file_record;  //do not modify the submitted record, this cast unlinks objects
785         $file_record = (object)$file_record; // we support arrays too
787         // validate all parameters, we do not want any rubbish stored in database, right?
788         if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
789             throw new file_exception('storedfileproblem', 'Invalid contextid');
790         }
792         if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
793             throw new file_exception('storedfileproblem', 'Invalid component');
794         }
796         if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
797             throw new file_exception('storedfileproblem', 'Invalid filearea');
798         }
800         if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
801             throw new file_exception('storedfileproblem', 'Invalid itemid');
802         }
804         if (!empty($file_record->sortorder)) {
805             if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
806                 $file_record->sortorder = 0;
807             }
808         } else {
809             $file_record->sortorder = 0;
810         }
812         $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
813         if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
814             // path must start and end with '/'
815             throw new file_exception('storedfileproblem', 'Invalid file path');
816         }
818         $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
819         if ($file_record->filename === '') {
820             // path must start and end with '/'
821             throw new file_exception('storedfileproblem', 'Invalid file name');
822         }
824         $now = time();
826         $newrecord = new object();
828         $newrecord->contextid = $file_record->contextid;
829         $newrecord->component = $file_record->component;
830         $newrecord->filearea  = $file_record->filearea;
831         $newrecord->itemid    = $file_record->itemid;
832         $newrecord->filepath  = $file_record->filepath;
833         $newrecord->filename  = $file_record->filename;
835         $newrecord->timecreated  = empty($file_record->timecreated) ? $now : $file_record->timecreated;
836         $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
837         $newrecord->mimetype     = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
838         $newrecord->userid       = empty($file_record->userid) ? null : $file_record->userid;
839         $newrecord->source       = empty($file_record->source) ? null : $file_record->source;
840         $newrecord->author       = empty($file_record->author) ? null : $file_record->author;
841         $newrecord->license      = empty($file_record->license) ? null : $file_record->license;
842         $newrecord->sortorder    = $file_record->sortorder;
844         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
846         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
848         try {
849             $newrecord->id = $DB->insert_record('files', $newrecord);
850         } catch (dml_exception $e) {
851             $newrecord->id = false;
852         }
854         if (!$newrecord->id) {
855             if ($newfile) {
856                 $this->deleted_file_cleanup($newrecord->contenthash);
857             }
858             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
859                                                     $newrecord->filepath, $newrecord->filename);
860         }
862         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
864         return new stored_file($this, $newrecord);
865     }
867     /**
868      * Creates new image file from existing.
869      *
870      * @param mixed $file_record object or array describing new file
871      * @param mixed file id or stored file object
872      * @param int $newwidth in pixels
873      * @param int $newheight in pixels
874      * @param bool $keepaspectratio
875      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
876      * @return stored_file instance
877      */
878     public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
879         if ($fid instanceof stored_file) {
880             $fid = $fid->get_id();
881         }
883         $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
885         if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
886             throw new file_exception('storedfileproblem', 'File does not exist');
887         }
889         if (!$imageinfo = $file->get_imageinfo()) {
890             throw new file_exception('storedfileproblem', 'File is not an image');
891         }
893         if (!isset($file_record['filename'])) {
894             $file_record['filename'] == $file->get_filename();
895         }
897         if (!isset($file_record['mimetype'])) {
898             $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
899         }
901         $width    = $imageinfo['width'];
902         $height   = $imageinfo['height'];
903         $mimetype = $imageinfo['mimetype'];
905         if ($keepaspectratio) {
906             if (0 >= $newwidth and 0 >= $newheight) {
907                 // no sizes specified
908                 $newwidth  = $width;
909                 $newheight = $height;
911             } else if (0 < $newwidth and 0 < $newheight) {
912                 $xheight = ($newwidth*($height/$width));
913                 if ($xheight < $newheight) {
914                     $newheight = (int)$xheight;
915                 } else {
916                     $newwidth = (int)($newheight*($width/$height));
917                 }
919             } else if (0 < $newwidth) {
920                 $newheight = (int)($newwidth*($height/$width));
922             } else { //0 < $newheight
923                 $newwidth = (int)($newheight*($width/$height));
924             }
926         } else {
927             if (0 >= $newwidth) {
928                 $newwidth = $width;
929             }
930             if (0 >= $newheight) {
931                 $newheight = $height;
932             }
933         }
935         $img = imagecreatefromstring($file->get_content());
936         if ($height != $newheight or $width != $newwidth) {
937             $newimg = imagecreatetruecolor($newwidth, $newheight);
938             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
939                 // weird
940                 throw new file_exception('storedfileproblem', 'Can not resize image');
941             }
942             imagedestroy($img);
943             $img = $newimg;
944         }
946         ob_start();
947         switch ($file_record['mimetype']) {
948             case 'image/gif':
949                 imagegif($img);
950                 break;
952             case 'image/jpeg':
953                 if (is_null($quality)) {
954                     imagejpeg($img);
955                 } else {
956                     imagejpeg($img, NULL, $quality);
957                 }
958                 break;
960             case 'image/png':
961                 $quality = (int)$quality;
962                 imagepng($img, NULL, $quality, NULL);
963                 break;
965             default:
966                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
967         }
969         $content = ob_get_contents();
970         ob_end_clean();
971         imagedestroy($img);
973         if (!$content) {
974             throw new file_exception('storedfileproblem', 'Can not convert image');
975         }
977         return $this->create_file_from_string($file_record, $content);
978     }
980     /**
981      * Add file content to sha1 pool.
982      *
983      * @param string $pathname path to file
984      * @param string $contenthash sha1 hash of content if known (performance only)
985      * @return array (contenthash, filesize, newfile)
986      */
987     public function add_file_to_pool($pathname, $contenthash = NULL) {
988         if (!is_readable($pathname)) {
989             throw new file_exception('storedfilecannotread');
990         }
992         if (is_null($contenthash)) {
993             $contenthash = sha1_file($pathname);
994         }
996         $filesize = filesize($pathname);
998         $hashpath = $this->path_from_hash($contenthash);
999         $hashfile = "$hashpath/$contenthash";
1001         if (file_exists($hashfile)) {
1002             if (filesize($hashfile) !== $filesize) {
1003                 throw new file_pool_content_exception($contenthash);
1004             }
1005             $newfile = false;
1007         } else {
1008             if (!is_dir($hashpath)) {
1009                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1010                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1011                 }
1012             }
1013             $newfile = true;
1015             if (!copy($pathname, $hashfile)) {
1016                 throw new file_exception('storedfilecannotread');
1017             }
1019             if (filesize($hashfile) !== $filesize) {
1020                 @unlink($hashfile);
1021                 throw new file_pool_content_exception($contenthash);
1022             }
1023             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1024         }
1027         return array($contenthash, $filesize, $newfile);
1028     }
1030     /**
1031      * Add string content to sha1 pool.
1032      *
1033      * @param string $content file content - binary string
1034      * @return array (contenthash, filesize, newfile)
1035      */
1036     public function add_string_to_pool($content) {
1037         $contenthash = sha1($content);
1038         $filesize = strlen($content); // binary length
1040         $hashpath = $this->path_from_hash($contenthash);
1041         $hashfile = "$hashpath/$contenthash";
1044         if (file_exists($hashfile)) {
1045             if (filesize($hashfile) !== $filesize) {
1046                 throw new file_pool_content_exception($contenthash);
1047             }
1048             $newfile = false;
1050         } else {
1051             if (!is_dir($hashpath)) {
1052                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1053                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1054                 }
1055             }
1056             $newfile = true;
1058             file_put_contents($hashfile, $content);
1060             if (filesize($hashfile) !== $filesize) {
1061                 @unlink($hashfile);
1062                 throw new file_pool_content_exception($contenthash);
1063             }
1064             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1065         }
1067         return array($contenthash, $filesize, $newfile);
1068     }
1070     /**
1071      * Return path to file with given hash.
1072      *
1073      * NOTE: must not be public, files in pool must not be modified
1074      *
1075      * @param string $contenthash
1076      * @return string expected file location
1077      */
1078     protected function path_from_hash($contenthash) {
1079         $l1 = $contenthash[0].$contenthash[1];
1080         $l2 = $contenthash[2].$contenthash[3];
1081         return "$this->filedir/$l1/$l2";
1082     }
1084     /**
1085      * Return path to file with given hash.
1086      *
1087      * NOTE: must not be public, files in pool must not be modified
1088      *
1089      * @param string $contenthash
1090      * @return string expected file location
1091      */
1092     protected function trash_path_from_hash($contenthash) {
1093         $l1 = $contenthash[0].$contenthash[1];
1094         $l2 = $contenthash[2].$contenthash[3];
1095         return "$this->trashdir/$l1/$l2";
1096     }
1098     /**
1099      * Tries to recover missing content of file from trash.
1100      *
1101      * @param object $file_record
1102      * @return bool success
1103      */
1104     public function try_content_recovery($file) {
1105         $contenthash = $file->get_contenthash();
1106         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1107         if (!is_readable($trashfile)) {
1108             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1109                 return false;
1110             }
1111             // nice, at least alternative trash file in trash root exists
1112             $trashfile = $this->trashdir.'/'.$contenthash;
1113         }
1114         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1115             //weird, better fail early
1116             return false;
1117         }
1118         $contentdir  = $this->path_from_hash($contenthash);
1119         $contentfile = $contentdir.'/'.$contenthash;
1120         if (file_exists($contentfile)) {
1121             //strange, no need to recover anything
1122             return true;
1123         }
1124         if (!is_dir($contentdir)) {
1125             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1126                 return false;
1127             }
1128         }
1129         return rename($trashfile, $contentfile);
1130     }
1132     /**
1133      * Marks pool file as candidate for deleting.
1134      *
1135      * DO NOT call directly - reserved for core!!
1136      *
1137      * @param string $contenthash
1138      * @return void
1139      */
1140     public function deleted_file_cleanup($contenthash) {
1141         global $DB;
1143         //Note: this section is critical - in theory file could be reused at the same
1144         //      time, if this happens we can still recover the file from trash
1145         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1146             // file content is still used
1147             return;
1148         }
1149         //move content file to trash
1150         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1151         if (!file_exists($contentfile)) {
1152             //weird, but no problem
1153             return;
1154         }
1155         $trashpath = $this->trash_path_from_hash($contenthash);
1156         $trashfile = $trashpath.'/'.$contenthash;
1157         if (file_exists($trashfile)) {
1158             // we already have this content in trash, no need to move it there
1159             unlink($contentfile);
1160             return;
1161         }
1162         if (!is_dir($trashpath)) {
1163             mkdir($trashpath, $this->dirpermissions, true);
1164         }
1165         rename($contentfile, $trashfile);
1166         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1167     }
1169     /**
1170      * Cron cleanup job.
1171      *
1172      * @return void
1173      */
1174     public function cron() {
1175         global $CFG, $DB;
1177         // find out all stale draft areas (older than 4 days) and purge them
1178         // those are identified by time stamp of the /. root dir
1179         mtrace('Deleting old draft files... ', '');
1180         $old = time() - 60*60*24*4;
1181         $sql = "SELECT *
1182                   FROM {files}
1183                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1184                        AND timecreated < :old";
1185         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1186         foreach ($rs as $dir) {
1187             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1188         }
1190         // remove trash pool files once a day
1191         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1192         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1193             require_once($CFG->libdir.'/filelib.php');
1194             // Delete files that are associated with a context that no longer exists.
1195             mtrace('Cleaning up files from deleted contexts... ', '');
1196             $sql = "SELECT DISTINCT f.contextid
1197                     FROM {files} f
1198                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
1199                     WHERE c.id IS NULL";
1200             if ($rs = $DB->get_recordset_sql($sql)) {
1201                 $fs = get_file_storage();
1202                 foreach ($rs as $ctx) {
1203                     $fs->delete_area_files($ctx->contextid);
1204                 }
1205             }
1206             mtrace('done.');
1208             mtrace('Deleting trash files... ', '');
1209             fulldelete($this->trashdir);
1210             set_config('fileslastcleanup', time());
1211             mtrace('done.');
1212         }
1213     }