MDL-24144 hiding of empty folders in repository/local
[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         global $DB;
205         $filepath = clean_param($filepath, PARAM_PATH);
206         $filename = clean_param($filename, PARAM_FILE);
208         if ($filename === '') {
209             $filename = '.';
210         }
212         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
213         return $this->get_file_by_hash($pathnamehash);
214     }
216     /**
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
223      * @return bool empty
224      */
225     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
226         global $DB;
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";
234         }
236         if ($ignoredirs) {
237             $sql = "SELECT 'x'
238                       FROM {files}
239                      WHERE $where AND filename <> '.'";
240         } else {
241             $sql = "SELECT 'x'
242                       FROM {files}
243                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
244         }
246         return !$DB->record_exists_sql($sql, $params);
247     }
249     /**
250      * Returns all area files (optionally limited by itemid)
251      *
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
259      */
260     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
261         global $DB;
263         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
264         if ($itemid !== false) {
265             $conditions['itemid'] = $itemid;
266         }
268         $result = array();
269         $file_records = $DB->get_records('files', $conditions, $sort);
270         foreach ($file_records as $file_record) {
271             if (!$includedirs and $file_record->filename === '.') {
272                 continue;
273             }
274             $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
275         }
276         return $result;
277     }
279     /**
280      * Returns array based tree structure of area files
281      *
282      * @param int $contextid
283      * @param string $component
284      * @param string $filearea
285      * @param int $itemid
286      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
287      */
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()) {
294                 continue;
295             }
296             unset($files[$hash]);
297             if ($dir->get_filepath() === '/') {
298                 $result['dirfile'] = $dir;
299                 continue;
300             }
301             $parts = explode('/', trim($dir->get_filepath(),'/'));
302             $pointer =& $result;
303             foreach ($parts as $part) {
304                 if ($part === '') {
305                     continue;
306                 }
307                 if (!isset($pointer['subdirs'][$part])) {
308                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
309                 }
310                 $pointer =& $pointer['subdirs'][$part];
311             }
312             $pointer['dirfile'] = $dir;
313             unset($pointer);
314         }
315         foreach ($files as $hash=>$file) {
316             $parts = explode('/', trim($file->get_filepath(),'/'));
317             $pointer =& $result;
318             foreach ($parts as $part) {
319                 if ($part === '') {
320                     continue;
321                 }
322                 $pointer =& $pointer['subdirs'][$part];
323             }
324             $pointer['files'][$file->get_filename()] = $file;
325             unset($pointer);
326         }
327         return $result;
328     }
330     /**
331      * Returns all files and optionally directories
332      *
333      * @param int $contextid
334      * @param string $component
335      * @param string $filearea
336      * @param int $itemid
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
342      */
343     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
344         global $DB;
346         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
347             return array();
348         }
350         if ($recursive) {
352             $dirs = $includedirs ? "" : "AND filename <> '.'";
353             $length = textlib_get_instance()->strlen($filepath);
355             $sql = "SELECT *
356                       FROM {files}
357                      WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
358                            AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
359                            AND id <> :dirid
360                            $dirs
361                   ORDER BY $sort";
362             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
364             $files = array();
365             $dirs  = array();
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);
370                 } else {
371                     $files[$file_record->pathnamehash] = new stored_file($this, $file_record);
372                 }
373             }
374             $result = array_merge($dirs, $files);
376         } else {
377             $result = array();
378             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
380             $length = textlib_get_instance()->strlen($filepath);
382             if ($includedirs) {
383                 $sql = "SELECT *
384                           FROM {files}
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
388                                AND id <> :dirid
389                       ORDER BY $sort";
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) {
394                         continue;
395                     }
396                     $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
397                 }
398             }
400             $sql = "SELECT *
401                       FROM {files}
402                      WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
403                            AND filepath = :filepath AND filename <> '.'
404                   ORDER BY $sort";
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);
409             }
410         }
412         return $result;
413     }
415     /**
416      * Delete all area files (optionally limited by itemid).
417      *
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
423      */
424     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
425         global $DB;
427         $conditions = array('contextid'=>$contextid);
428         if ($component !== false) {
429             $conditions['component'] = $component;
430         }
431         if ($filearea !== false) {
432             $conditions['filearea'] = $filearea;
433         }
434         if ($itemid !== false) {
435             $conditions['itemid'] = $itemid;
436         }
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();
442         }
444         return true; // BC only
445     }
447     /**
448      * Recursively creates directory.
449      *
450      * @param int $contextid
451      * @param string $component
452      * @param string $filearea
453      * @param int $itemid
454      * @param string $filepath
455      * @param string $filename
456      * @return bool success
457      */
458     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
459         global $DB;
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');
464         }
466         if ($component === '' or $component !== clean_param($component, PARAM_ALPHAEXT)) {
467             throw new file_exception('storedfileproblem', 'Invalid component');
468         }
470         if ($filearea === '' or $filearea !== clean_param($filearea, PARAM_ALPHAEXT)) {
471             throw new file_exception('storedfileproblem', 'Invalid filearea');
472         }
474         if (!is_number($itemid) or $itemid < 0) {
475             throw new file_exception('storedfileproblem', 'Invalid itemid');
476         }
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');
482         }
484         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
486         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
487             return $dir_info;
488         }
490         static $contenthash = null;
491         if (!$contenthash) {
492             $this->add_string_to_pool('');
493             $contenthash = sha1('');
494         }
496         $now = time();
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);
526         }
528         return $dir_info;
529     }
531     /**
532      * Add new local file based on existing local file.
533      *
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
537      */
538     public function create_file_from_storedfile($file_record, $fileorid) {
539         global $DB;
541         if ($fileorid instanceof stored_file) {
542             $fid = $fileorid->get_id();
543         } else {
544             $fid = $fileorid;
545         }
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']);
554         $now = time();
556         if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
557             throw new file_exception('storedfileproblem', 'File does not exist');
558         }
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');
566             }
568             if ($key == 'component') {
569                 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
570                     throw new file_exception('storedfileproblem', 'Invalid component');
571                 }
572             }
574             if ($key == 'filearea') {
575                 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
576                     throw new file_exception('storedfileproblem', 'Invalid filearea');
577                 }
578             }
580             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
581                 throw new file_exception('storedfileproblem', 'Invalid itemid');
582             }
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');
590                 }
591             }
593             if ($key == 'filename') {
594                 $value = clean_param($value, PARAM_FILE);
595                 if ($value === '') {
596                     // path must start and end with '/'
597                     throw new file_exception('storedfileproblem', 'Invalid file name');
598                 }
599             }
601             $newrecord->$key = $value;
602         }
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);
613         }
615         try {
616             $newrecord->id = $DB->insert_record('files', $newrecord);
617         } catch (database_exception $e) {
618             $newrecord->id = false;
619         }
621         if (!$newrecord->id) {
622             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
623                                                      $newrecord->filepath, $newrecord->filename);
624         }
626         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
628         return new stored_file($this, $newrecord);
629     }
631     /**
632      * Add new local file.
633      *
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
639      */
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);
656         }
657         $source = !empty($file_record->source) ? $file_record->source : $url;
658         $file_record->source = clean_param($source, PARAM_URL);
660         if ($usetempfile) {
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');
666             }
667             try {
668                 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
669                 @unlink($tmpfile);
670                 return $newfile;
671             } catch (Exception $e) {
672                 @unlink($tmpfile);
673                 throw $e;
674             }
676         } else {
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');
680             }
681             return $this->create_file_from_string($file_record, $content);
682         }
683     }
685     /**
686      * Add new local file.
687      *
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
691      */
692     public function create_file_from_pathname($file_record, $pathname) {
693         global $DB;
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');
701         }
703         if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
704             throw new file_exception('storedfileproblem', 'Invalid component');
705         }
707         if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
708             throw new file_exception('storedfileproblem', 'Invalid filearea');
709         }
711         if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
712             throw new file_exception('storedfileproblem', 'Invalid itemid');
713         }
715         if (!empty($file_record->sortorder)) {
716             if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
717                 $file_record->sortorder = 0;
718             }
719         } else {
720             $file_record->sortorder = 0;
721         }
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');
727         }
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');
733         }
735         $now = time();
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);
759         try {
760             $newrecord->id = $DB->insert_record('files', $newrecord);
761         } catch (database_exception $e) {
762             $newrecord->id = false;
763         }
765         if (!$newrecord->id) {
766             if ($newfile) {
767                 $this->deleted_file_cleanup($newrecord->contenthash);
768             }
769             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
770                                                     $newrecord->filepath, $newrecord->filename);
771         }
773         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
775         return new stored_file($this, $newrecord);
776     }
778     /**
779      * Add new local file.
780      *
781      * @param mixed $file_record object or array describing file
782      * @param string $content content of file
783      * @return stored_file instance
784      */
785     public function create_file_from_string($file_record, $content) {
786         global $DB;
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');
794         }
796         if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
797             throw new file_exception('storedfileproblem', 'Invalid component');
798         }
800         if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
801             throw new file_exception('storedfileproblem', 'Invalid filearea');
802         }
804         if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
805             throw new file_exception('storedfileproblem', 'Invalid itemid');
806         }
808         if (!empty($file_record->sortorder)) {
809             if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
810                 $file_record->sortorder = 0;
811             }
812         } else {
813             $file_record->sortorder = 0;
814         }
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');
820         }
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');
826         }
828         $now = time();
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);
852         try {
853             $newrecord->id = $DB->insert_record('files', $newrecord);
854         } catch (database_exception $e) {
855             $newrecord->id = false;
856         }
858         if (!$newrecord->id) {
859             if ($newfile) {
860                 $this->deleted_file_cleanup($newrecord->contenthash);
861             }
862             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
863                                                     $newrecord->filepath, $newrecord->filename);
864         }
866         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
868         return new stored_file($this, $newrecord);
869     }
871     /**
872      * Creates new image file from existing.
873      *
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
881      */
882     public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
883         global $DB;
885         if ($fid instanceof stored_file) {
886             $fid = $fid->get_id();
887         }
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');
893         }
895         if (!$imageinfo = $file->get_imageinfo()) {
896             throw new file_exception('storedfileproblem', 'File is not an image');
897         }
899         if (!isset($file_record['filename'])) {
900             $file_record['filename'] == $file->get_filename();
901         }
903         if (!isset($file_record['mimetype'])) {
904             $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
905         }
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
914                 $newwidth  = $width;
915                 $newheight = $height;
917             } else if (0 < $newwidth and 0 < $newheight) {
918                 $xheight = ($newwidth*($height/$width));
919                 if ($xheight < $newheight) {
920                     $newheight = (int)$xheight;
921                 } else {
922                     $newwidth = (int)($newheight*($width/$height));
923                 }
925             } else if (0 < $newwidth) {
926                 $newheight = (int)($newwidth*($height/$width));
928             } else { //0 < $newheight
929                 $newwidth = (int)($newheight*($width/$height));
930             }
932         } else {
933             if (0 >= $newwidth) {
934                 $newwidth = $width;
935             }
936             if (0 >= $newheight) {
937                 $newheight = $height;
938             }
939         }
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)) {
945                 // weird
946                 throw new file_exception('storedfileproblem', 'Can not resize image');
947             }
948             imagedestroy($img);
949             $img = $newimg;
950         }
952         ob_start();
953         switch ($file_record['mimetype']) {
954             case 'image/gif':
955                 imagegif($img);
956                 break;
958             case 'image/jpeg':
959                 if (is_null($quality)) {
960                     imagejpeg($img);
961                 } else {
962                     imagejpeg($img, NULL, $quality);
963                 }
964                 break;
966             case 'image/png':
967                 $quality = (int)$quality;
968                 imagepng($img, NULL, $quality, NULL);
969                 break;
971             default:
972                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
973         }
975         $content = ob_get_contents();
976         ob_end_clean();
977         imagedestroy($img);
979         if (!$content) {
980             throw new file_exception('storedfileproblem', 'Can not convert image');
981         }
983         return $this->create_file_from_string($file_record, $content);
984     }
986     /**
987      * Add file content to sha1 pool.
988      *
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)
992      */
993     public function add_file_to_pool($pathname, $contenthash = NULL) {
994         if (!is_readable($pathname)) {
995             throw new file_exception('storedfilecannotread');
996         }
998         if (is_null($contenthash)) {
999             $contenthash = sha1_file($pathname);
1000         }
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);
1010             }
1011             $newfile = false;
1013         } else {
1014             if (!is_dir($hashpath)) {
1015                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1016                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1017                 }
1018             }
1019             $newfile = true;
1021             if (!copy($pathname, $hashfile)) {
1022                 throw new file_exception('storedfilecannotread');
1023             }
1025             if (filesize($hashfile) !== $filesize) {
1026                 @unlink($hashfile);
1027                 throw new file_pool_content_exception($contenthash);
1028             }
1029             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1030         }
1033         return array($contenthash, $filesize, $newfile);
1034     }
1036     /**
1037      * Add string content to sha1 pool.
1038      *
1039      * @param string $content file content - binary string
1040      * @return array (contenthash, filesize, newfile)
1041      */
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);
1053             }
1054             $newfile = false;
1056         } else {
1057             if (!is_dir($hashpath)) {
1058                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1059                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1060                 }
1061             }
1062             $newfile = true;
1064             file_put_contents($hashfile, $content);
1066             if (filesize($hashfile) !== $filesize) {
1067                 @unlink($hashfile);
1068                 throw new file_pool_content_exception($contenthash);
1069             }
1070             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1071         }
1073         return array($contenthash, $filesize, $newfile);
1074     }
1076     /**
1077      * Return path to file with given hash.
1078      *
1079      * NOTE: must not be public, files in pool must not be modified
1080      *
1081      * @param string $contenthash
1082      * @return string expected file location
1083      */
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";
1088     }
1090     /**
1091      * Return path to file with given hash.
1092      *
1093      * NOTE: must not be public, files in pool must not be modified
1094      *
1095      * @param string $contenthash
1096      * @return string expected file location
1097      */
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";
1102     }
1104     /**
1105      * Tries to recover missing content of file from trash.
1106      *
1107      * @param object $file_record
1108      * @return bool success
1109      */
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)) {
1115                 return false;
1116             }
1117             // nice, at least alternative trash file in trash root exists
1118             $trashfile = $this->trashdir.'/'.$contenthash;
1119         }
1120         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1121             //weird, better fail early
1122             return false;
1123         }
1124         $contentdir  = $this->path_from_hash($contenthash);
1125         $contentfile = $contentdir.'/'.$contenthash;
1126         if (file_exists($contentfile)) {
1127             //strange, no need to recover anything
1128             return true;
1129         }
1130         if (!is_dir($contentdir)) {
1131             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1132                 return false;
1133             }
1134         }
1135         return rename($trashfile, $contentfile);
1136     }
1138     /**
1139      * Marks pool file as candidate for deleting.
1140      *
1141      * DO NOT call directly - reserved for core!!
1142      *
1143      * @param string $contenthash
1144      * @return void
1145      */
1146     public function deleted_file_cleanup($contenthash) {
1147         global $DB;
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
1153             return;
1154         }
1155         //move content file to trash
1156         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1157         if (!file_exists($contentfile)) {
1158             //weird, but no problem
1159             return;
1160         }
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);
1166             return;
1167         }
1168         if (!is_dir($trashpath)) {
1169             mkdir($trashpath, $this->dirpermissions, true);
1170         }
1171         rename($contentfile, $trashfile);
1172         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1173     }
1175     /**
1176      * Cron cleanup job.
1177      *
1178      * @return void
1179      */
1180     public function cron() {
1181         global $CFG, $DB;
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;
1187         $sql = "SELECT *
1188                   FROM {files}
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);
1194         }
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
1203                     FROM {files} f
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);
1210                 }
1211             }
1212             mtrace('done.');
1214             mtrace('Deleting trash files... ', '');
1215             fulldelete($this->trashdir);
1216             set_config('fileslastcleanup', time());
1217             mtrace('done.');
1218         }
1219     }