2a875fa48e7bfb58016dd91a30a6bc26567f9920
[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      * Returns all area files (optionally limited by itemid)
218      *
219      * @param int $contextid
220      * @param string $component
221      * @param string $filearea
222      * @param int $itemid (all files if not specified)
223      * @param string $sort
224      * @param bool $includedirs
225      * @return array of stored_files indexed by pathanmehash
226      */
227     public function get_area_files($contextid, $component, $filearea, $itemid=false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
228         global $DB;
230         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
231         if ($itemid !== false) {
232             $conditions['itemid'] = $itemid;
233         }
235         $result = array();
236         $file_records = $DB->get_records('files', $conditions, $sort);
237         foreach ($file_records as $file_record) {
238             if (!$includedirs and $file_record->filename === '.') {
239                 continue;
240             }
241             $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
242         }
243         return $result;
244     }
246     /**
247      * Returns array based tree structure of area files
248      *
249      * @param int $contextid
250      * @param string $component
251      * @param string $filearea
252      * @param int $itemid
253      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
254      */
255     public function get_area_tree($contextid, $component, $filearea, $itemid) {
256         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
257         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, $sort="sortorder, itemid, filepath, filename", true);
258         // first create directory structure
259         foreach ($files as $hash=>$dir) {
260             if (!$dir->is_directory()) {
261                 continue;
262             }
263             unset($files[$hash]);
264             if ($dir->get_filepath() === '/') {
265                 $result['dirfile'] = $dir;
266                 continue;
267             }
268             $parts = explode('/', trim($dir->get_filepath(),'/'));
269             $pointer =& $result;
270             foreach ($parts as $part) {
271                 if ($part === '') {
272                     continue;
273                 }
274                 if (!isset($pointer['subdirs'][$part])) {
275                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
276                 }
277                 $pointer =& $pointer['subdirs'][$part];
278             }
279             $pointer['dirfile'] = $dir;
280             unset($pointer);
281         }
282         foreach ($files as $hash=>$file) {
283             $parts = explode('/', trim($file->get_filepath(),'/'));
284             $pointer =& $result;
285             foreach ($parts as $part) {
286                 if ($part === '') {
287                     continue;
288                 }
289                 $pointer =& $pointer['subdirs'][$part];
290             }
291             $pointer['files'][$file->get_filename()] = $file;
292             unset($pointer);
293         }
294         return $result;
295     }
297     /**
298      * Returns all files and optionally directories
299      *
300      * @param int $contextid
301      * @param string $component
302      * @param string $filearea
303      * @param int $itemid
304      * @param int $filepath directory path
305      * @param bool $recursive include all subdirectories
306      * @param bool $includedirs include files and directories
307      * @param string $sort
308      * @return array of stored_files indexed by pathanmehash
309      */
310     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
311         global $DB;
313         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
314             return array();
315         }
317         if ($recursive) {
319             $dirs = $includedirs ? "" : "AND filename <> '.'";
320             $length = textlib_get_instance()->strlen($filepath);
322             $sql = "SELECT *
323                       FROM {files}
324                      WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
325                            AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
326                            AND id <> :dirid
327                            $dirs
328                   ORDER BY $sort";
329             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
331             $files = array();
332             $dirs  = array();
333             $file_records = $DB->get_records_sql($sql, $params);
334             foreach ($file_records as $file_record) {
335                 if ($file_record->filename == '.') {
336                     $dirs[$file_record->pathnamehash] = new stored_file($this, $file_record);
337                 } else {
338                     $files[$file_record->pathnamehash] = new stored_file($this, $file_record);
339                 }
340             }
341             $result = array_merge($dirs, $files);
343         } else {
344             $result = array();
345             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
347             $length = textlib_get_instance()->strlen($filepath);
349             if ($includedirs) {
350                 $sql = "SELECT *
351                           FROM {files}
352                          WHERE contextid = :contextid AND component = :component AND filearea = :filearea
353                                AND itemid = :itemid AND filename = '.'
354                                AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
355                                AND id <> :dirid
356                       ORDER BY $sort";
357                 $reqlevel = substr_count($filepath, '/') + 1;
358                 $file_records = $DB->get_records_sql($sql, $params);
359                 foreach ($file_records as $file_record) {
360                     if (substr_count($file_record->filepath, '/') !== $reqlevel) {
361                         continue;
362                     }
363                     $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
364                 }
365             }
367             $sql = "SELECT *
368                       FROM {files}
369                      WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
370                            AND filepath = :filepath AND filename <> '.'
371                   ORDER BY $sort";
373             $file_records = $DB->get_records_sql($sql, $params);
374             foreach ($file_records as $file_record) {
375                 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
376             }
377         }
379         return $result;
380     }
382     /**
383      * Delete all area files (optionally limited by itemid).
384      *
385      * @param int $contextid
386      * @param string $component
387      * @param string $filearea (all areas in context if not specified)
388      * @param int $itemid (all files if not specified)
389      * @return bool success
390      */
391     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
392         global $DB;
394         $conditions = array('contextid'=>$contextid);
395         if ($component !== false) {
396             $conditions['component'] = $component;
397         }
398         if ($filearea !== false) {
399             $conditions['filearea'] = $filearea;
400         }
401         if ($itemid !== false) {
402             $conditions['itemid'] = $itemid;
403         }
405         $file_records = $DB->get_records('files', $conditions);
406         foreach ($file_records as $file_record) {
407             $stored_file = new stored_file($this, $file_record);
408             $stored_file->delete();
409         }
411         return true; // BC only
412     }
414     /**
415      * Recursively creates directory.
416      *
417      * @param int $contextid
418      * @param string $component
419      * @param string $filearea
420      * @param int $itemid
421      * @param string $filepath
422      * @param string $filename
423      * @return bool success
424      */
425     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
426         global $DB;
428         // validate all parameters, we do not want any rubbish stored in database, right?
429         if (!is_number($contextid) or $contextid < 1) {
430             throw new file_exception('storedfileproblem', 'Invalid contextid');
431         }
433         if ($component === '' or $component !== clean_param($component, PARAM_ALPHAEXT)) {
434             throw new file_exception('storedfileproblem', 'Invalid component');
435         }
437         if ($filearea === '' or $filearea !== clean_param($filearea, PARAM_ALPHAEXT)) {
438             throw new file_exception('storedfileproblem', 'Invalid filearea');
439         }
441         if (!is_number($itemid) or $itemid < 0) {
442             throw new file_exception('storedfileproblem', 'Invalid itemid');
443         }
445         $filepath = clean_param($filepath, PARAM_PATH);
446         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
447             // path must start and end with '/'
448             throw new file_exception('storedfileproblem', 'Invalid file path');
449         }
451         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
453         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
454             return $dir_info;
455         }
457         static $contenthash = null;
458         if (!$contenthash) {
459             $this->add_string_to_pool('');
460             $contenthash = sha1('');
461         }
463         $now = time();
465         $dir_record = new object();
466         $dir_record->contextid = $contextid;
467         $dir_record->component = $component;
468         $dir_record->filearea  = $filearea;
469         $dir_record->itemid    = $itemid;
470         $dir_record->filepath  = $filepath;
471         $dir_record->filename  = '.';
472         $dir_record->contenthash  = $contenthash;
473         $dir_record->filesize  = 0;
475         $dir_record->timecreated  = $now;
476         $dir_record->timemodified = $now;
477         $dir_record->mimetype     = null;
478         $dir_record->userid       = $userid;
480         $dir_record->pathnamehash = $pathnamehash;
482         $DB->insert_record('files', $dir_record);
483         $dir_info = $this->get_file_by_hash($pathnamehash);
485         if ($filepath !== '/') {
486             //recurse to parent dirs
487             $filepath = trim($filepath, '/');
488             $filepath = explode('/', $filepath);
489             array_pop($filepath);
490             $filepath = implode('/', $filepath);
491             $filepath = ($filepath === '') ? '/' : "/$filepath/";
492             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
493         }
495         return $dir_info;
496     }
498     /**
499      * Add new local file based on existing local file.
500      *
501      * @param mixed $file_record object or array describing changes
502      * @param mixed $fileorid id or stored_file instance of the existing local file
503      * @return stored_file instance of newly created file
504      */
505     public function create_file_from_storedfile($file_record, $fileorid) {
506         global $DB;
508         if ($fileorid instanceof stored_file) {
509             $fid = $fileorid->get_id();
510         } else {
511             $fid = $fileorid;
512         }
514         $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
516         unset($file_record['id']);
517         unset($file_record['filesize']);
518         unset($file_record['contenthash']);
519         unset($file_record['pathnamehash']);
521         $now = time();
523         if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
524             throw new file_exception('storedfileproblem', 'File does not exist');
525         }
527         unset($newrecord->id);
529         foreach ($file_record as $key=>$value) {
530             // validate all parameters, we do not want any rubbish stored in database, right?
531             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
532                 throw new file_exception('storedfileproblem', 'Invalid contextid');
533             }
535             if ($key == 'component') {
536                 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
537                     throw new file_exception('storedfileproblem', 'Invalid component');
538                 }
539             }
541             if ($key == 'filearea') {
542                 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
543                     throw new file_exception('storedfileproblem', 'Invalid filearea');
544                 }
545             }
547             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
548                 throw new file_exception('storedfileproblem', 'Invalid itemid');
549             }
552             if ($key == 'filepath') {
553                 $value = clean_param($value, PARAM_PATH);
554                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
555                     // path must start and end with '/'
556                     throw new file_exception('storedfileproblem', 'Invalid file path');
557                 }
558             }
560             if ($key == 'filename') {
561                 $value = clean_param($value, PARAM_FILE);
562                 if ($value === '') {
563                     // path must start and end with '/'
564                     throw new file_exception('storedfileproblem', 'Invalid file name');
565                 }
566             }
568             $newrecord->$key = $value;
569         }
571         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
573         if ($newrecord->filename === '.') {
574             // special case - only this function supports directories ;-)
575             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
576             // update the existing directory with the new data
577             $newrecord->id = $directory->get_id();
578             $DB->update_record('files', $newrecord);
579             return new stored_file($this, $newrecord);
580         }
582         try {
583             $newrecord->id = $DB->insert_record('files', $newrecord);
584         } catch (database_exception $e) {
585             $newrecord->id = false;
586         }
588         if (!$newrecord->id) {
589             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
590                                                      $newrecord->filepath, $newrecord->filename);
591         }
593         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
595         return new stored_file($this, $newrecord);
596     }
598     /**
599      * Add new local file.
600      *
601      * @param mixed $file_record object or array describing file
602      * @param string $path path to file or content of file
603      * @param array $options @see download_file_content() options
604      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
605      * @return stored_file instance
606      */
607     public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
609         $file_record = (array)$file_record;  //do not modify the submitted record, this cast unlinks objects
610         $file_record = (object)$file_record; // we support arrays too
612         $headers        = isset($options['headers'])        ? $options['headers'] : null;
613         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
614         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
615         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
616         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
617         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
619         if (!isset($file_record->filename)) {
620             $parts = explode('/', $url);
621             $filename = array_pop($parts);
622             $file_record->filename = clean_param($filename, PARAM_FILE);
623         }
624         $source = !empty($file_record->source) ? $file_record->source : $url;
625         $file_record->source = clean_param($source, PARAM_URL);
627         if ($usetempfile) {
628             check_dir_exists($this->tempdir, true, true);
629             $tmpfile = tempnam($this->tempdir, 'newfromurl');
630             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile);
631             if ($content === false) {
632                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
633             }
634             try {
635                 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
636                 @unlink($tmpfile);
637                 return $newfile;
638             } catch (Exception $e) {
639                 @unlink($tmpfile);
640                 throw $e;
641             }
643         } else {
644             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify);
645             if ($content === false) {
646                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
647             }
648             return $this->create_file_from_string($file_record, $content);
649         }
650     }
652     /**
653      * Add new local file.
654      *
655      * @param mixed $file_record object or array describing file
656      * @param string $path path to file or content of file
657      * @return stored_file instance
658      */
659     public function create_file_from_pathname($file_record, $pathname) {
660         global $DB;
662         $file_record = (array)$file_record;  //do not modify the submitted record, this cast unlinks objects
663         $file_record = (object)$file_record; // we support arrays too
665         // validate all parameters, we do not want any rubbish stored in database, right?
666         if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
667             throw new file_exception('storedfileproblem', 'Invalid contextid');
668         }
670         if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
671             throw new file_exception('storedfileproblem', 'Invalid component');
672         }
674         if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
675             throw new file_exception('storedfileproblem', 'Invalid filearea');
676         }
678         if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
679             throw new file_exception('storedfileproblem', 'Invalid itemid');
680         }
682         if (!empty($file_record->sortorder)) {
683             if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
684                 $file_record->sortorder = 0;
685             }
686         } else {
687             $file_record->sortorder = 0;
688         }
690         $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
691         if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
692             // path must start and end with '/'
693             throw new file_exception('storedfileproblem', 'Invalid file path');
694         }
696         $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
697         if ($file_record->filename === '') {
698             // filename must not be empty
699             throw new file_exception('storedfileproblem', 'Invalid file name');
700         }
702         $now = time();
704         $newrecord = new object();
706         $newrecord->contextid = $file_record->contextid;
707         $newrecord->component = $file_record->component;
708         $newrecord->filearea  = $file_record->filearea;
709         $newrecord->itemid    = $file_record->itemid;
710         $newrecord->filepath  = $file_record->filepath;
711         $newrecord->filename  = $file_record->filename;
713         $newrecord->timecreated  = empty($file_record->timecreated) ? $now : $file_record->timecreated;
714         $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
715         $newrecord->mimetype     = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
716         $newrecord->userid       = empty($file_record->userid) ? null : $file_record->userid;
717         $newrecord->source       = empty($file_record->source) ? null : $file_record->source;
718         $newrecord->author       = empty($file_record->author) ? null : $file_record->author;
719         $newrecord->license      = empty($file_record->license) ? null : $file_record->license;
720         $newrecord->sortorder    = $file_record->sortorder;
722         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
724         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
726         try {
727             $newrecord->id = $DB->insert_record('files', $newrecord);
728         } catch (database_exception $e) {
729             $newrecord->id = false;
730         }
732         if (!$newrecord->id) {
733             if ($newfile) {
734                 $this->deleted_file_cleanup($newrecord->contenthash);
735             }
736             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
737                                                     $newrecord->filepath, $newrecord->filename);
738         }
740         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
742         return new stored_file($this, $newrecord);
743     }
745     /**
746      * Add new local file.
747      *
748      * @param mixed $file_record object or array describing file
749      * @param string $content content of file
750      * @return stored_file instance
751      */
752     public function create_file_from_string($file_record, $content) {
753         global $DB;
755         $file_record = (array)$file_record;  //do not modify the submitted record, this cast unlinks objects
756         $file_record = (object)$file_record; // we support arrays too
758         // validate all parameters, we do not want any rubbish stored in database, right?
759         if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
760             throw new file_exception('storedfileproblem', 'Invalid contextid');
761         }
763         if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
764             throw new file_exception('storedfileproblem', 'Invalid component');
765         }
767         if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
768             throw new file_exception('storedfileproblem', 'Invalid filearea');
769         }
771         if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
772             throw new file_exception('storedfileproblem', 'Invalid itemid');
773         }
775         if (!empty($file_record->sortorder)) {
776             if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
777                 $file_record->sortorder = 0;
778             }
779         } else {
780             $file_record->sortorder = 0;
781         }
783         $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
784         if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
785             // path must start and end with '/'
786             throw new file_exception('storedfileproblem', 'Invalid file path');
787         }
789         $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
790         if ($file_record->filename === '') {
791             // path must start and end with '/'
792             throw new file_exception('storedfileproblem', 'Invalid file name');
793         }
795         $now = time();
797         $newrecord = new object();
799         $newrecord->contextid = $file_record->contextid;
800         $newrecord->component = $file_record->component;
801         $newrecord->filearea  = $file_record->filearea;
802         $newrecord->itemid    = $file_record->itemid;
803         $newrecord->filepath  = $file_record->filepath;
804         $newrecord->filename  = $file_record->filename;
806         $newrecord->timecreated  = empty($file_record->timecreated) ? $now : $file_record->timecreated;
807         $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
808         $newrecord->mimetype     = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
809         $newrecord->userid       = empty($file_record->userid) ? null : $file_record->userid;
810         $newrecord->source       = empty($file_record->source) ? null : $file_record->source;
811         $newrecord->author       = empty($file_record->author) ? null : $file_record->author;
812         $newrecord->license      = empty($file_record->license) ? null : $file_record->license;
813         $newrecord->sortorder    = $file_record->sortorder;
815         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
817         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
819         try {
820             $newrecord->id = $DB->insert_record('files', $newrecord);
821         } catch (database_exception $e) {
822             $newrecord->id = false;
823         }
825         if (!$newrecord->id) {
826             if ($newfile) {
827                 $this->deleted_file_cleanup($newrecord->contenthash);
828             }
829             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
830                                                     $newrecord->filepath, $newrecord->filename);
831         }
833         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
835         return new stored_file($this, $newrecord);
836     }
838     /**
839      * Creates new image file from existing.
840      *
841      * @param mixed $file_record object or array describing new file
842      * @param mixed file id or stored file object
843      * @param int $newwidth in pixels
844      * @param int $newheight in pixels
845      * @param bool $keepaspectratio
846      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
847      * @return stored_file instance
848      */
849     public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
850         global $DB;
852         if ($fid instanceof stored_file) {
853             $fid = $fid->get_id();
854         }
856         $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
858         if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
859             throw new file_exception('storedfileproblem', 'File does not exist');
860         }
862         if (!$imageinfo = $file->get_imageinfo()) {
863             throw new file_exception('storedfileproblem', 'File is not an image');
864         }
866         if (!isset($file_record['filename'])) {
867             $file_record['filename'] == $file->get_filename();
868         }
870         if (!isset($file_record['mimetype'])) {
871             $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
872         }
874         $width    = $imageinfo['width'];
875         $height   = $imageinfo['height'];
876         $mimetype = $imageinfo['mimetype'];
878         if ($keepaspectratio) {
879             if (0 >= $newwidth and 0 >= $newheight) {
880                 // no sizes specified
881                 $newwidth  = $width;
882                 $newheight = $height;
884             } else if (0 < $newwidth and 0 < $newheight) {
885                 $xheight = ($newwidth*($height/$width));
886                 if ($xheight < $newheight) {
887                     $newheight = (int)$xheight;
888                 } else {
889                     $newwidth = (int)($newheight*($width/$height));
890                 }
892             } else if (0 < $newwidth) {
893                 $newheight = (int)($newwidth*($height/$width));
895             } else { //0 < $newheight
896                 $newwidth = (int)($newheight*($width/$height));
897             }
899         } else {
900             if (0 >= $newwidth) {
901                 $newwidth = $width;
902             }
903             if (0 >= $newheight) {
904                 $newheight = $height;
905             }
906         }
908         $img = imagecreatefromstring($file->get_content());
909         if ($height != $newheight or $width != $newwidth) {
910             $newimg = imagecreatetruecolor($newwidth, $newheight);
911             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
912                 // weird
913                 throw new file_exception('storedfileproblem', 'Can not resize image');
914             }
915             imagedestroy($img);
916             $img = $newimg;
917         }
919         ob_start();
920         switch ($file_record['mimetype']) {
921             case 'image/gif':
922                 imagegif($img);
923                 break;
925             case 'image/jpeg':
926                 if (is_null($quality)) {
927                     imagejpeg($img);
928                 } else {
929                     imagejpeg($img, NULL, $quality);
930                 }
931                 break;
933             case 'image/png':
934                 $quality = (int)$quality;
935                 imagepng($img, NULL, $quality, NULL);
936                 break;
938             default:
939                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
940         }
942         $content = ob_get_contents();
943         ob_end_clean();
944         imagedestroy($img);
946         if (!$content) {
947             throw new file_exception('storedfileproblem', 'Can not convert image');
948         }
950         return $this->create_file_from_string($file_record, $content);
951     }
953     /**
954      * Add file content to sha1 pool.
955      *
956      * @param string $pathname path to file
957      * @param string $contenthash sha1 hash of content if known (performance only)
958      * @return array (contenthash, filesize, newfile)
959      */
960     public function add_file_to_pool($pathname, $contenthash = NULL) {
961         if (!is_readable($pathname)) {
962             throw new file_exception('storedfilecannotread');
963         }
965         if (is_null($contenthash)) {
966             $contenthash = sha1_file($pathname);
967         }
969         $filesize = filesize($pathname);
971         $hashpath = $this->path_from_hash($contenthash);
972         $hashfile = "$hashpath/$contenthash";
974         if (file_exists($hashfile)) {
975             if (filesize($hashfile) !== $filesize) {
976                 throw new file_pool_content_exception($contenthash);
977             }
978             $newfile = false;
980         } else {
981             if (!is_dir($hashpath)) {
982                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
983                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
984                 }
985             }
986             $newfile = true;
988             if (!copy($pathname, $hashfile)) {
989                 throw new file_exception('storedfilecannotread');
990             }
992             if (filesize($hashfile) !== $filesize) {
993                 @unlink($hashfile);
994                 throw new file_pool_content_exception($contenthash);
995             }
996             chmod($hashfile, $this->filepermissions); // fix permissions if needed
997         }
1000         return array($contenthash, $filesize, $newfile);
1001     }
1003     /**
1004      * Add string content to sha1 pool.
1005      *
1006      * @param string $content file content - binary string
1007      * @return array (contenthash, filesize, newfile)
1008      */
1009     public function add_string_to_pool($content) {
1010         $contenthash = sha1($content);
1011         $filesize = strlen($content); // binary length
1013         $hashpath = $this->path_from_hash($contenthash);
1014         $hashfile = "$hashpath/$contenthash";
1017         if (file_exists($hashfile)) {
1018             if (filesize($hashfile) !== $filesize) {
1019                 throw new file_pool_content_exception($contenthash);
1020             }
1021             $newfile = false;
1023         } else {
1024             if (!is_dir($hashpath)) {
1025                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1026                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1027                 }
1028             }
1029             $newfile = true;
1031             file_put_contents($hashfile, $content);
1033             if (filesize($hashfile) !== $filesize) {
1034                 @unlink($hashfile);
1035                 throw new file_pool_content_exception($contenthash);
1036             }
1037             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1038         }
1040         return array($contenthash, $filesize, $newfile);
1041     }
1043     /**
1044      * Return path to file with given hash.
1045      *
1046      * NOTE: must not be public, files in pool must not be modified
1047      *
1048      * @param string $contenthash
1049      * @return string expected file location
1050      */
1051     protected function path_from_hash($contenthash) {
1052         $l1 = $contenthash[0].$contenthash[1];
1053         $l2 = $contenthash[2].$contenthash[3];
1054         $l3 = $contenthash[4].$contenthash[5];
1055         return "$this->filedir/$l1/$l2/$l3";
1056     }
1058     /**
1059      * Return path to file with given hash.
1060      *
1061      * NOTE: must not be public, files in pool must not be modified
1062      *
1063      * @param string $contenthash
1064      * @return string expected file location
1065      */
1066     protected function trash_path_from_hash($contenthash) {
1067         $l1 = $contenthash[0].$contenthash[1];
1068         $l2 = $contenthash[2].$contenthash[3];
1069         $l3 = $contenthash[4].$contenthash[5];
1070         return "$this->trashdir/$l1/$l2/$l3";
1071     }
1073     /**
1074      * Tries to recover missing content of file from trash.
1075      *
1076      * @param object $file_record
1077      * @return bool success
1078      */
1079     public function try_content_recovery($file) {
1080         $contenthash = $file->get_contenthash();
1081         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1082         if (!is_readable($trashfile)) {
1083             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1084                 return false;
1085             }
1086             // nice, at least alternative trash file in trash root exists
1087             $trashfile = $this->trashdir.'/'.$contenthash;
1088         }
1089         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1090             //weird, better fail early
1091             return false;
1092         }
1093         $contentdir  = $this->path_from_hash($contenthash);
1094         $contentfile = $contentdir.'/'.$contenthash;
1095         if (file_exists($contentfile)) {
1096             //strange, no need to recover anything
1097             return true;
1098         }
1099         if (!is_dir($contentdir)) {
1100             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1101                 return false;
1102             }
1103         }
1104         return rename($trashfile, $contentfile);
1105     }
1107     /**
1108      * Marks pool file as candidate for deleting.
1109      *
1110      * DO NOT call directly - reserved for core!!
1111      *
1112      * @param string $contenthash
1113      * @return void
1114      */
1115     public function deleted_file_cleanup($contenthash) {
1116         global $DB;
1118         //Note: this section is critical - in theory file could be reused at the same
1119         //      time, if this happens we can still recover the file from trash
1120         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1121             // file content is still used
1122             return;
1123         }
1124         //move content file to trash
1125         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1126         if (!file_exists($contentfile)) {
1127             //weird, but no problem
1128             return;
1129         }
1130         $trashpath = $this->trash_path_from_hash($contenthash);
1131         $trashfile = $trashpath.'/'.$contenthash;
1132         if (file_exists($trashfile)) {
1133             // we already have this content in trash, no need to move it there
1134             unlink($contentfile);
1135             return;
1136         }
1137         if (!is_dir($trashpath)) {
1138             mkdir($trashpath, $this->dirpermissions, true);
1139         }
1140         rename($contentfile, $trashfile);
1141         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1142     }
1144     /**
1145      * Cron cleanup job.
1146      *
1147      * @return void
1148      */
1149     public function cron() {
1150         global $CFG, $DB;
1152         // find out all stale draft areas (older than 4 days) and purge them
1153         // those are identified by time stamp of the /. root dir
1154         mtrace('Deleting old draft files... ', '');
1155         $old = time() - 60*60*24*4;
1156         $sql = "SELECT *
1157                   FROM {files}
1158                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1159                        AND timecreated < :old";
1160         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1161         foreach ($rs as $dir) {
1162             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1163         }
1165         // remove trash pool files once a day
1166         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1167         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1168             require_once($CFG->libdir.'/filelib.php');
1169             // Delete files that are associated with a context that no longer exists.
1170             mtrace('Cleaning up files from deleted contexts... ', '');
1171             $sql = "SELECT DISTINCT f.contextid
1172                     FROM {files} f
1173                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
1174                     WHERE c.id IS NULL";
1175             if ($rs = $DB->get_recordset_sql($sql)) {
1176                 $fs = get_file_storage();
1177                 foreach ($rs as $ctx) {
1178                     $fs->delete_area_files($ctx->contextid);
1179                 }
1180             }
1181             mtrace('done.');
1183             mtrace('Deleting trash files... ', '');
1184             fulldelete($this->trashdir);
1185             set_config('fileslastcleanup', time());
1186             mtrace('done.');
1187         }
1188     }