5fac72679af2c15aebd00c10b01fc5855e3db504
[moodle.git] / lib / filestorage / file_storage.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Core file storage class definition.
20  *
21  * @package   core_files
22  * @copyright 2008 Petr Skoda {@link http://skodak.org}
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/stored_file.php");
30 /**
31  * File storage class used for low level access to stored files.
32  *
33  * Only owner of file area may use this class to access own files,
34  * for example only code in mod/assignment/* may access assignment
35  * attachments. When some other part of moodle needs to access
36  * files of modules it has to use file_browser class instead or there
37  * has to be some callback API.
38  *
39  * @package   core_files
40  * @category  files
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 {@link 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      * Calculates sha1 hash of unique full path name information.
94      *
95      * This hash is a unique file identifier - it is used to improve
96      * performance and overcome db index size limits.
97      *
98      * @param int $contextid context ID
99      * @param string $component component
100      * @param string $filearea file area
101      * @param int $itemid item ID
102      * @param string $filepath file path
103      * @param string $filename file name
104      * @return string sha1 hash
105      */
106     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
108     }
110     /**
111      * Does this file exist?
112      *
113      * @param int $contextid context ID
114      * @param string $component component
115      * @param string $filearea file area
116      * @param int $itemid item ID
117      * @param string $filepath file path
118      * @param string $filename file name
119      * @return bool
120      */
121     public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
122         $filepath = clean_param($filepath, PARAM_PATH);
123         $filename = clean_param($filename, PARAM_FILE);
125         if ($filename === '') {
126             $filename = '.';
127         }
129         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
130         return $this->file_exists_by_hash($pathnamehash);
131     }
133     /**
134      * Whether or not the file exist
135      *
136      * @param string $pathnamehash path name hash
137      * @return bool
138      */
139     public function file_exists_by_hash($pathnamehash) {
140         global $DB;
142         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
143     }
145     /**
146      * Create instance of file class from database record.
147      *
148      * @param stdClass $filerecord record from the files table left join files_reference table
149      * @return stored_file instance of file abstraction class
150      */
151     public function get_file_instance(stdClass $filerecord) {
152         $storedfile = new stored_file($this, $filerecord, $this->filedir);
153         return $storedfile;
154     }
156     /**
157      * Returns an image file that represent the given stored file as a preview
158      *
159      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
160      * future, the support for other mimetypes can be added, too (eg. generate an image
161      * preview of PDF, text documents etc).
162      *
163      * @param stored_file $file the file we want to preview
164      * @param string $mode preview mode, eg. 'thumb'
165      * @return stored_file|bool false if unable to create the preview, stored file otherwise
166      */
167     public function get_file_preview(stored_file $file, $mode) {
169         $context = context_system::instance();
170         $path = '/' . trim($mode, '/') . '/';
171         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
173         if (!$preview) {
174             $preview = $this->create_file_preview($file, $mode);
175             if (!$preview) {
176                 return false;
177             }
178         }
180         return $preview;
181     }
183     /**
184      * Generates a preview image for the stored file
185      *
186      * @param stored_file $file the file we want to preview
187      * @param string $mode preview mode, eg. 'thumb'
188      * @return stored_file|bool the newly created preview file or false
189      */
190     protected function create_file_preview(stored_file $file, $mode) {
192         $mimetype = $file->get_mimetype();
194         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
195             // make a preview of the image
196             $data = $this->create_imagefile_preview($file, $mode);
198         } else {
199             // unable to create the preview of this mimetype yet
200             return false;
201         }
203         if (empty($data)) {
204             return false;
205         }
207         // getimagesizefromstring() is available from PHP 5.4 but we need to support
208         // lower versions, so...
209         $tmproot = make_temp_directory('thumbnails');
210         $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
211         file_put_contents($tmpfilepath, $data);
212         $imageinfo = getimagesize($tmpfilepath);
213         unlink($tmpfilepath);
215         $context = context_system::instance();
217         $record = array(
218             'contextid' => $context->id,
219             'component' => 'core',
220             'filearea'  => 'preview',
221             'itemid'    => 0,
222             'filepath'  => '/' . trim($mode, '/') . '/',
223             'filename'  => $file->get_contenthash(),
224         );
226         if ($imageinfo) {
227             $record['mimetype'] = $imageinfo['mime'];
228         }
230         return $this->create_file_from_string($record, $data);
231     }
233     /**
234      * Generates a preview for the stored image file
235      *
236      * @param stored_file $file the image we want to preview
237      * @param string $mode preview mode, eg. 'thumb'
238      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
239      */
240     protected function create_imagefile_preview(stored_file $file, $mode) {
241         global $CFG;
242         require_once($CFG->libdir.'/gdlib.php');
244         $tmproot = make_temp_directory('thumbnails');
245         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
246         $file->copy_content_to($tmpfilepath);
248         if ($mode === 'tinyicon') {
249             $data = generate_image_thumbnail($tmpfilepath, 24, 24);
251         } else if ($mode === 'thumb') {
252             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
254         } else {
255             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
256         }
258         unlink($tmpfilepath);
260         return $data;
261     }
263     /**
264      * Fetch file using local file id.
265      *
266      * Please do not rely on file ids, it is usually easier to use
267      * pathname hashes instead.
268      *
269      * @param int $fileid file ID
270      * @return stored_file|bool stored_file instance if exists, false if not
271      */
272     public function get_file_by_id($fileid) {
273         global $DB;
275         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
276                   FROM {files} f
277              LEFT JOIN {files_reference} r
278                        ON f.referencefileid = r.id
279                  WHERE f.id = ?";
280         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
281             return $this->get_file_instance($filerecord);
282         } else {
283             return false;
284         }
285     }
287     /**
288      * Fetch file using local file full pathname hash
289      *
290      * @param string $pathnamehash path name hash
291      * @return stored_file|bool stored_file instance if exists, false if not
292      */
293     public function get_file_by_hash($pathnamehash) {
294         global $DB;
296         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
297                   FROM {files} f
298              LEFT JOIN {files_reference} r
299                        ON f.referencefileid = r.id
300                  WHERE f.pathnamehash = ?";
301         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
302             return $this->get_file_instance($filerecord);
303         } else {
304             return false;
305         }
306     }
308     /**
309      * Fetch locally stored file.
310      *
311      * @param int $contextid context ID
312      * @param string $component component
313      * @param string $filearea file area
314      * @param int $itemid item ID
315      * @param string $filepath file path
316      * @param string $filename file name
317      * @return stored_file|bool stored_file instance if exists, false if not
318      */
319     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
320         $filepath = clean_param($filepath, PARAM_PATH);
321         $filename = clean_param($filename, PARAM_FILE);
323         if ($filename === '') {
324             $filename = '.';
325         }
327         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
328         return $this->get_file_by_hash($pathnamehash);
329     }
331     /**
332      * Are there any files (or directories)
333      *
334      * @param int $contextid context ID
335      * @param string $component component
336      * @param string $filearea file area
337      * @param bool|int $itemid item id or false if all items
338      * @param bool $ignoredirs whether or not ignore directories
339      * @return bool empty
340      */
341     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
342         global $DB;
344         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
345         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
347         if ($itemid !== false) {
348             $params['itemid'] = $itemid;
349             $where .= " AND itemid = :itemid";
350         }
352         if ($ignoredirs) {
353             $sql = "SELECT 'x'
354                       FROM {files}
355                      WHERE $where AND filename <> '.'";
356         } else {
357             $sql = "SELECT 'x'
358                       FROM {files}
359                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
360         }
362         return !$DB->record_exists_sql($sql, $params);
363     }
365     /**
366      * Returns all files belonging to given repository
367      *
368      * @param int $repositoryid
369      * @param string $sort
370      */
371     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
372         global $DB;
373         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
374                   FROM {files} f
375              LEFT JOIN {files_reference} r
376                        ON f.referencefileid = r.id
377                  WHERE r.repositoryid = ?
378               ORDER BY $sort";
380         $result = array();
381         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
382         foreach ($filerecords as $filerecord) {
383             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
384         }
385         return $result;
386     }
388     /**
389      * Returns all area files (optionally limited by itemid)
390      *
391      * @param int $contextid context ID
392      * @param string $component component
393      * @param string $filearea file area
394      * @param int $itemid item ID or all files if not specified
395      * @param string $sort sort fields
396      * @param bool $includedirs whether or not include directories
397      * @return array of stored_files indexed by pathanmehash
398      */
399     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
400         global $DB;
402         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
403         if ($itemid !== false) {
404             $itemidsql = ' AND f.itemid = :itemid ';
405             $conditions['itemid'] = $itemid;
406         } else {
407             $itemidsql = '';
408         }
410         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
411                   FROM {files} f
412              LEFT JOIN {files_reference} r
413                        ON f.referencefileid = r.id
414                  WHERE f.contextid = :contextid
415                        AND f.component = :component
416                        AND f.filearea = :filearea
417                        $itemidsql
418               ORDER BY $sort";
420         $result = array();
421         $filerecords = $DB->get_records_sql($sql, $conditions);
422         foreach ($filerecords as $filerecord) {
423             if (!$includedirs and $filerecord->filename === '.') {
424                 continue;
425             }
426             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
427         }
428         return $result;
429     }
431     /**
432      * Returns array based tree structure of area files
433      *
434      * @param int $contextid context ID
435      * @param string $component component
436      * @param string $filearea file area
437      * @param int $itemid item ID
438      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
439      */
440     public function get_area_tree($contextid, $component, $filearea, $itemid) {
441         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
442         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
443         // first create directory structure
444         foreach ($files as $hash=>$dir) {
445             if (!$dir->is_directory()) {
446                 continue;
447             }
448             unset($files[$hash]);
449             if ($dir->get_filepath() === '/') {
450                 $result['dirfile'] = $dir;
451                 continue;
452             }
453             $parts = explode('/', trim($dir->get_filepath(),'/'));
454             $pointer =& $result;
455             foreach ($parts as $part) {
456                 if ($part === '') {
457                     continue;
458                 }
459                 if (!isset($pointer['subdirs'][$part])) {
460                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
461                 }
462                 $pointer =& $pointer['subdirs'][$part];
463             }
464             $pointer['dirfile'] = $dir;
465             unset($pointer);
466         }
467         foreach ($files as $hash=>$file) {
468             $parts = explode('/', trim($file->get_filepath(),'/'));
469             $pointer =& $result;
470             foreach ($parts as $part) {
471                 if ($part === '') {
472                     continue;
473                 }
474                 $pointer =& $pointer['subdirs'][$part];
475             }
476             $pointer['files'][$file->get_filename()] = $file;
477             unset($pointer);
478         }
479         return $result;
480     }
482     /**
483      * Returns all files and optionally directories
484      *
485      * @param int $contextid context ID
486      * @param string $component component
487      * @param string $filearea file area
488      * @param int $itemid item ID
489      * @param int $filepath directory path
490      * @param bool $recursive include all subdirectories
491      * @param bool $includedirs include files and directories
492      * @param string $sort sort fields
493      * @return array of stored_files indexed by pathanmehash
494      */
495     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
496         global $DB;
498         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
499             return array();
500         }
502         if ($recursive) {
504             $dirs = $includedirs ? "" : "AND filename <> '.'";
505             $length = textlib::strlen($filepath);
507             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
508                       FROM {files} f
509                  LEFT JOIN {files_reference} r
510                            ON f.referencefileid = r.id
511                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
512                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
513                            AND f.id <> :dirid
514                            $dirs
515                   ORDER BY $sort";
516             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
518             $files = array();
519             $dirs  = array();
520             $filerecords = $DB->get_records_sql($sql, $params);
521             foreach ($filerecords as $filerecord) {
522                 if ($filerecord->filename == '.') {
523                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
524                 } else {
525                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
526                 }
527             }
528             $result = array_merge($dirs, $files);
530         } else {
531             $result = array();
532             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
534             $length = textlib::strlen($filepath);
536             if ($includedirs) {
537                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
538                           FROM {files} f
539                      LEFT JOIN {files_reference} r
540                                ON f.referencefileid = r.id
541                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
542                                AND f.itemid = :itemid AND f.filename = '.'
543                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
544                                AND f.id <> :dirid
545                       ORDER BY $sort";
546                 $reqlevel = substr_count($filepath, '/') + 1;
547                 $filerecords = $DB->get_records_sql($sql, $params);
548                 foreach ($filerecords as $filerecord) {
549                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
550                         continue;
551                     }
552                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
553                 }
554             }
556             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
557                       FROM {files} f
558                  LEFT JOIN {files_reference} r
559                            ON f.referencefileid = r.id
560                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
561                            AND f.filepath = :filepath AND f.filename <> '.'
562                   ORDER BY $sort";
564             $filerecords = $DB->get_records_sql($sql, $params);
565             foreach ($filerecords as $filerecord) {
566                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
567             }
568         }
570         return $result;
571     }
573     /**
574      * Delete all area files (optionally limited by itemid).
575      *
576      * @param int $contextid context ID
577      * @param string $component component
578      * @param string $filearea file area or all areas in context if not specified
579      * @param int $itemid item ID or all files if not specified
580      * @return bool success
581      */
582     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
583         global $DB;
585         $conditions = array('contextid'=>$contextid);
586         if ($component !== false) {
587             $conditions['component'] = $component;
588         }
589         if ($filearea !== false) {
590             $conditions['filearea'] = $filearea;
591         }
592         if ($itemid !== false) {
593             $conditions['itemid'] = $itemid;
594         }
596         $filerecords = $DB->get_records('files', $conditions);
597         foreach ($filerecords as $filerecord) {
598             $this->get_file_instance($filerecord)->delete();
599         }
601         return true; // BC only
602     }
604     /**
605      * Delete all the files from certain areas where itemid is limited by an
606      * arbitrary bit of SQL.
607      *
608      * @param int $contextid the id of the context the files belong to. Must be given.
609      * @param string $component the owning component. Must be given.
610      * @param string $filearea the file area name. Must be given.
611      * @param string $itemidstest an SQL fragment that the itemid must match. Used
612      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
613      *      and may not used named parameters called contextid, component or filearea.
614      * @param array $params any query params used by $itemidstest.
615      */
616     public function delete_area_files_select($contextid, $component,
617             $filearea, $itemidstest, array $params = null) {
618         global $DB;
620         $where = "contextid = :contextid
621                 AND component = :component
622                 AND filearea = :filearea
623                 AND itemid $itemidstest";
624         $params['contextid'] = $contextid;
625         $params['component'] = $component;
626         $params['filearea'] = $filearea;
628         $filerecords = $DB->get_recordset_select('files', $where, $params);
629         foreach ($filerecords as $filerecord) {
630             $this->get_file_instance($filerecord)->delete();
631         }
632         $filerecords->close();
633     }
635     /**
636      * Move all the files in a file area from one context to another.
637      *
638      * @param int $oldcontextid the context the files are being moved from.
639      * @param int $newcontextid the context the files are being moved to.
640      * @param string $component the plugin that these files belong to.
641      * @param string $filearea the name of the file area.
642      * @param int $itemid file item ID
643      * @return int the number of files moved, for information.
644      */
645     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
646         // Note, this code is based on some code that Petr wrote in
647         // forum_move_attachments in mod/forum/lib.php. I moved it here because
648         // I needed it in the question code too.
649         $count = 0;
651         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
652         foreach ($oldfiles as $oldfile) {
653             $filerecord = new stdClass();
654             $filerecord->contextid = $newcontextid;
655             $this->create_file_from_storedfile($filerecord, $oldfile);
656             $count += 1;
657         }
659         if ($count) {
660             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
661         }
663         return $count;
664     }
666     /**
667      * Recursively creates directory.
668      *
669      * @param int $contextid context ID
670      * @param string $component component
671      * @param string $filearea file area
672      * @param int $itemid item ID
673      * @param string $filepath file path
674      * @param int $userid the user ID
675      * @return bool success
676      */
677     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
678         global $DB;
680         // validate all parameters, we do not want any rubbish stored in database, right?
681         if (!is_number($contextid) or $contextid < 1) {
682             throw new file_exception('storedfileproblem', 'Invalid contextid');
683         }
685         $component = clean_param($component, PARAM_COMPONENT);
686         if (empty($component)) {
687             throw new file_exception('storedfileproblem', 'Invalid component');
688         }
690         $filearea = clean_param($filearea, PARAM_AREA);
691         if (empty($filearea)) {
692             throw new file_exception('storedfileproblem', 'Invalid filearea');
693         }
695         if (!is_number($itemid) or $itemid < 0) {
696             throw new file_exception('storedfileproblem', 'Invalid itemid');
697         }
699         $filepath = clean_param($filepath, PARAM_PATH);
700         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
701             // path must start and end with '/'
702             throw new file_exception('storedfileproblem', 'Invalid file path');
703         }
705         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
707         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
708             return $dir_info;
709         }
711         static $contenthash = null;
712         if (!$contenthash) {
713             $this->add_string_to_pool('');
714             $contenthash = sha1('');
715         }
717         $now = time();
719         $dir_record = new stdClass();
720         $dir_record->contextid = $contextid;
721         $dir_record->component = $component;
722         $dir_record->filearea  = $filearea;
723         $dir_record->itemid    = $itemid;
724         $dir_record->filepath  = $filepath;
725         $dir_record->filename  = '.';
726         $dir_record->contenthash  = $contenthash;
727         $dir_record->filesize  = 0;
729         $dir_record->timecreated  = $now;
730         $dir_record->timemodified = $now;
731         $dir_record->mimetype     = null;
732         $dir_record->userid       = $userid;
734         $dir_record->pathnamehash = $pathnamehash;
736         $DB->insert_record('files', $dir_record);
737         $dir_info = $this->get_file_by_hash($pathnamehash);
739         if ($filepath !== '/') {
740             //recurse to parent dirs
741             $filepath = trim($filepath, '/');
742             $filepath = explode('/', $filepath);
743             array_pop($filepath);
744             $filepath = implode('/', $filepath);
745             $filepath = ($filepath === '') ? '/' : "/$filepath/";
746             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
747         }
749         return $dir_info;
750     }
752     /**
753      * Add new local file based on existing local file.
754      *
755      * @param stdClass|array $filerecord object or array describing changes
756      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
757      * @return stored_file instance of newly created file
758      */
759     public function create_file_from_storedfile($filerecord, $fileorid) {
760         global $DB;
762         if ($fileorid instanceof stored_file) {
763             $fid = $fileorid->get_id();
764         } else {
765             $fid = $fileorid;
766         }
768         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
770         unset($filerecord['id']);
771         unset($filerecord['filesize']);
772         unset($filerecord['contenthash']);
773         unset($filerecord['pathnamehash']);
775         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
776                   FROM {files} f
777              LEFT JOIN {files_reference} r
778                        ON f.referencefileid = r.id
779                  WHERE f.id = ?";
781         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
782             throw new file_exception('storedfileproblem', 'File does not exist');
783         }
785         unset($newrecord->id);
787         foreach ($filerecord as $key => $value) {
788             // validate all parameters, we do not want any rubbish stored in database, right?
789             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
790                 throw new file_exception('storedfileproblem', 'Invalid contextid');
791             }
793             if ($key == 'component') {
794                 $value = clean_param($value, PARAM_COMPONENT);
795                 if (empty($value)) {
796                     throw new file_exception('storedfileproblem', 'Invalid component');
797                 }
798             }
800             if ($key == 'filearea') {
801                 $value = clean_param($value, PARAM_AREA);
802                 if (empty($value)) {
803                     throw new file_exception('storedfileproblem', 'Invalid filearea');
804                 }
805             }
807             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
808                 throw new file_exception('storedfileproblem', 'Invalid itemid');
809             }
812             if ($key == 'filepath') {
813                 $value = clean_param($value, PARAM_PATH);
814                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
815                     // path must start and end with '/'
816                     throw new file_exception('storedfileproblem', 'Invalid file path');
817                 }
818             }
820             if ($key == 'filename') {
821                 $value = clean_param($value, PARAM_FILE);
822                 if ($value === '') {
823                     // path must start and end with '/'
824                     throw new file_exception('storedfileproblem', 'Invalid file name');
825                 }
826             }
828             if ($key === 'timecreated' or $key === 'timemodified') {
829                 if (!is_number($value)) {
830                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
831                 }
832                 if ($value < 0) {
833                     //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
834                     $value = 0;
835                 }
836             }
838             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
839                 $value = clean_param($value, PARAM_INT);
840             }
842             $newrecord->$key = $value;
843         }
845         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
847         if ($newrecord->filename === '.') {
848             // special case - only this function supports directories ;-)
849             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
850             // update the existing directory with the new data
851             $newrecord->id = $directory->get_id();
852             $DB->update_record('files', $newrecord);
853             return $this->get_file_instance($newrecord);
854         }
856         if (!empty($newrecord->repositoryid)) {
857             try {
858                 $referencerecord = new stdClass;
859                 $referencerecord->repositoryid = $newrecord->repositoryid;
860                 $referencerecord->reference = $newrecord->reference;
861                 $referencerecord->lastsync  = $newrecord->referencelastsync;
862                 $referencerecord->lifetime  = $newrecord->referencelifetime;
863                 $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
864             } catch (dml_exception $e) {
865                 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
866                                                          $newrecord->filepath, $newrecord->filename, $e->debuginfo);
867             }
868             $newrecord->referencefileid = $referencerecord->id;
869         }
871         try {
872             $newrecord->id = $DB->insert_record('files', $newrecord);
873         } catch (dml_exception $e) {
874             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
875                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
876         }
879         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
881         return $this->get_file_instance($newrecord);
882     }
884     /**
885      * Add new local file.
886      *
887      * @param stdClass|array $filerecord object or array describing file
888      * @param string $url the URL to the file
889      * @param array $options {@link download_file_content()} options
890      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
891      * @return stored_file
892      */
893     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
895         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
896         $filerecord = (object)$filerecord; // We support arrays too.
898         $headers        = isset($options['headers'])        ? $options['headers'] : null;
899         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
900         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
901         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
902         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
903         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
904         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
906         if (!isset($filerecord->filename)) {
907             $parts = explode('/', $url);
908             $filename = array_pop($parts);
909             $filerecord->filename = clean_param($filename, PARAM_FILE);
910         }
911         $source = !empty($filerecord->source) ? $filerecord->source : $url;
912         $filerecord->source = clean_param($source, PARAM_URL);
914         if ($usetempfile) {
915             check_dir_exists($this->tempdir);
916             $tmpfile = tempnam($this->tempdir, 'newfromurl');
917             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
918             if ($content === false) {
919                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
920             }
921             try {
922                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
923                 @unlink($tmpfile);
924                 return $newfile;
925             } catch (Exception $e) {
926                 @unlink($tmpfile);
927                 throw $e;
928             }
930         } else {
931             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
932             if ($content === false) {
933                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
934             }
935             return $this->create_file_from_string($filerecord, $content);
936         }
937     }
939     /**
940      * Add new local file.
941      *
942      * @param stdClass|array $filerecord object or array describing file
943      * @param string $pathname path to file or content of file
944      * @return stored_file
945      */
946     public function create_file_from_pathname($filerecord, $pathname) {
947         global $DB;
949         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
950         $filerecord = (object)$filerecord; // We support arrays too.
952         // validate all parameters, we do not want any rubbish stored in database, right?
953         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
954             throw new file_exception('storedfileproblem', 'Invalid contextid');
955         }
957         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
958         if (empty($filerecord->component)) {
959             throw new file_exception('storedfileproblem', 'Invalid component');
960         }
962         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
963         if (empty($filerecord->filearea)) {
964             throw new file_exception('storedfileproblem', 'Invalid filearea');
965         }
967         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
968             throw new file_exception('storedfileproblem', 'Invalid itemid');
969         }
971         if (!empty($filerecord->sortorder)) {
972             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
973                 $filerecord->sortorder = 0;
974             }
975         } else {
976             $filerecord->sortorder = 0;
977         }
979         $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
980         $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
981         $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
983         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
984         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
985             // path must start and end with '/'
986             throw new file_exception('storedfileproblem', 'Invalid file path');
987         }
989         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
990         if ($filerecord->filename === '') {
991             // filename must not be empty
992             throw new file_exception('storedfileproblem', 'Invalid file name');
993         }
995         $now = time();
996         if (isset($filerecord->timecreated)) {
997             if (!is_number($filerecord->timecreated)) {
998                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
999             }
1000             if ($filerecord->timecreated < 0) {
1001                 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1002                 $filerecord->timecreated = 0;
1003             }
1004         } else {
1005             $filerecord->timecreated = $now;
1006         }
1008         if (isset($filerecord->timemodified)) {
1009             if (!is_number($filerecord->timemodified)) {
1010                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1011             }
1012             if ($filerecord->timemodified < 0) {
1013                 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1014                 $filerecord->timemodified = 0;
1015             }
1016         } else {
1017             $filerecord->timemodified = $now;
1018         }
1020         $newrecord = new stdClass();
1022         $newrecord->contextid = $filerecord->contextid;
1023         $newrecord->component = $filerecord->component;
1024         $newrecord->filearea  = $filerecord->filearea;
1025         $newrecord->itemid    = $filerecord->itemid;
1026         $newrecord->filepath  = $filerecord->filepath;
1027         $newrecord->filename  = $filerecord->filename;
1029         $newrecord->timecreated  = $filerecord->timecreated;
1030         $newrecord->timemodified = $filerecord->timemodified;
1031         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1032         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1033         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1034         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1035         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1036         $newrecord->sortorder    = $filerecord->sortorder;
1038         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1040         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1042         try {
1043             $newrecord->id = $DB->insert_record('files', $newrecord);
1044         } catch (dml_exception $e) {
1045             if ($newfile) {
1046                 $this->deleted_file_cleanup($newrecord->contenthash);
1047             }
1048             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1049                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1050         }
1052         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1054         return $this->get_file_instance($newrecord);
1055     }
1057     /**
1058      * Add new local file.
1059      *
1060      * @param stdClass|array $filerecord object or array describing file
1061      * @param string $content content of file
1062      * @return stored_file
1063      */
1064     public function create_file_from_string($filerecord, $content) {
1065         global $DB;
1067         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1068         $filerecord = (object)$filerecord; // We support arrays too.
1070         // validate all parameters, we do not want any rubbish stored in database, right?
1071         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1072             throw new file_exception('storedfileproblem', 'Invalid contextid');
1073         }
1075         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1076         if (empty($filerecord->component)) {
1077             throw new file_exception('storedfileproblem', 'Invalid component');
1078         }
1080         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1081         if (empty($filerecord->filearea)) {
1082             throw new file_exception('storedfileproblem', 'Invalid filearea');
1083         }
1085         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1086             throw new file_exception('storedfileproblem', 'Invalid itemid');
1087         }
1089         if (!empty($filerecord->sortorder)) {
1090             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1091                 $filerecord->sortorder = 0;
1092             }
1093         } else {
1094             $filerecord->sortorder = 0;
1095         }
1096         $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
1097         $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
1098         $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
1100         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1101         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1102             // path must start and end with '/'
1103             throw new file_exception('storedfileproblem', 'Invalid file path');
1104         }
1106         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1107         if ($filerecord->filename === '') {
1108             // path must start and end with '/'
1109             throw new file_exception('storedfileproblem', 'Invalid file name');
1110         }
1112         $now = time();
1113         if (isset($filerecord->timecreated)) {
1114             if (!is_number($filerecord->timecreated)) {
1115                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1116             }
1117             if ($filerecord->timecreated < 0) {
1118                 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1119                 $filerecord->timecreated = 0;
1120             }
1121         } else {
1122             $filerecord->timecreated = $now;
1123         }
1125         if (isset($filerecord->timemodified)) {
1126             if (!is_number($filerecord->timemodified)) {
1127                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1128             }
1129             if ($filerecord->timemodified < 0) {
1130                 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1131                 $filerecord->timemodified = 0;
1132             }
1133         } else {
1134             $filerecord->timemodified = $now;
1135         }
1137         $newrecord = new stdClass();
1139         $newrecord->contextid = $filerecord->contextid;
1140         $newrecord->component = $filerecord->component;
1141         $newrecord->filearea  = $filerecord->filearea;
1142         $newrecord->itemid    = $filerecord->itemid;
1143         $newrecord->filepath  = $filerecord->filepath;
1144         $newrecord->filename  = $filerecord->filename;
1146         $newrecord->timecreated  = $filerecord->timecreated;
1147         $newrecord->timemodified = $filerecord->timemodified;
1148         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1149         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1150         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1151         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1152         $newrecord->sortorder    = $filerecord->sortorder;
1154         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1155         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1156         // get mimetype by magic bytes
1157         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1159         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1161         try {
1162             $newrecord->id = $DB->insert_record('files', $newrecord);
1163         } catch (dml_exception $e) {
1164             if ($newfile) {
1165                 $this->deleted_file_cleanup($newrecord->contenthash);
1166             }
1167             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1168                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1169         }
1171         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1173         return $this->get_file_instance($newrecord);
1174     }
1176     /**
1177      * Create a moodle file from file reference information
1178      *
1179      * @param stdClass $filerecord
1180      * @param int $repositoryid
1181      * @param string $reference
1182      * @param array $options options for creating external file
1183      * @return stored_file
1184      */
1185     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1186         global $DB;
1188         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1189         $filerecord = (object)$filerecord; // We support arrays too.
1191         // validate all parameters, we do not want any rubbish stored in database, right?
1192         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1193             throw new file_exception('storedfileproblem', 'Invalid contextid');
1194         }
1196         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1197         if (empty($filerecord->component)) {
1198             throw new file_exception('storedfileproblem', 'Invalid component');
1199         }
1201         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1202         if (empty($filerecord->filearea)) {
1203             throw new file_exception('storedfileproblem', 'Invalid filearea');
1204         }
1206         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1207             throw new file_exception('storedfileproblem', 'Invalid itemid');
1208         }
1210         if (!empty($filerecord->sortorder)) {
1211             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1212                 $filerecord->sortorder = 0;
1213             }
1214         } else {
1215             $filerecord->sortorder = 0;
1216         }
1218         $filerecord->referencefileid   = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
1219         $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
1220         $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
1221         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1222         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1223         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1224         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1225         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1226         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1227         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1228             // Path must start and end with '/'.
1229             throw new file_exception('storedfileproblem', 'Invalid file path');
1230         }
1232         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1233         if ($filerecord->filename === '') {
1234             // Path must start and end with '/'.
1235             throw new file_exception('storedfileproblem', 'Invalid file name');
1236         }
1238         $now = time();
1239         if (isset($filerecord->timecreated)) {
1240             if (!is_number($filerecord->timecreated)) {
1241                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1242             }
1243             if ($filerecord->timecreated < 0) {
1244                 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1245                 $filerecord->timecreated = 0;
1246             }
1247         } else {
1248             $filerecord->timecreated = $now;
1249         }
1251         if (isset($filerecord->timemodified)) {
1252             if (!is_number($filerecord->timemodified)) {
1253                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1254             }
1255             if ($filerecord->timemodified < 0) {
1256                 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1257                 $filerecord->timemodified = 0;
1258             }
1259         } else {
1260             $filerecord->timemodified = $now;
1261         }
1263         // Insert file reference record.
1264         try {
1265             $referencerecord = new stdClass;
1266             $referencerecord->repositoryid = $repositoryid;
1267             $referencerecord->reference = $reference;
1268             $referencerecord->lastsync = $filerecord->referencelastsync;
1269             $referencerecord->lifetime = $filerecord->referencelifetime;
1270             $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
1271         } catch (dml_exception $e) {
1272             throw $e;
1273         }
1275         $filerecord->referencefileid = $referencerecord->id;
1277         // External file doesn't have content in moodle.
1278         // So we create an empty file for it.
1279         list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1281         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1283         try {
1284             $filerecord->id = $DB->insert_record('files', $filerecord);
1285         } catch (dml_exception $e) {
1286             if ($newfile) {
1287                 $this->deleted_file_cleanup($filerecord->contenthash);
1288             }
1289             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1290                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1291         }
1293         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1295         // Adding repositoryid and reference to file record to create stored_file instance
1296         $filerecord->repositoryid = $repositoryid;
1297         $filerecord->reference = $reference;
1298         return $this->get_file_instance($filerecord);
1299     }
1301     /**
1302      * Creates new image file from existing.
1303      *
1304      * @param stdClass|array $filerecord object or array describing new file
1305      * @param int|stored_file $fid file id or stored file object
1306      * @param int $newwidth in pixels
1307      * @param int $newheight in pixels
1308      * @param bool $keepaspectratio whether or not keep aspect ratio
1309      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1310      * @return stored_file
1311      */
1312     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1313         if (!function_exists('imagecreatefromstring')) {
1314             //Most likely the GD php extension isn't installed
1315             //image conversion cannot succeed
1316             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1317         }
1319         if ($fid instanceof stored_file) {
1320             $fid = $fid->get_id();
1321         }
1323         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1325         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1326             throw new file_exception('storedfileproblem', 'File does not exist');
1327         }
1329         if (!$imageinfo = $file->get_imageinfo()) {
1330             throw new file_exception('storedfileproblem', 'File is not an image');
1331         }
1333         if (!isset($filerecord['filename'])) {
1334             $filerecord['filename'] = $file->get_filename();
1335         }
1337         if (!isset($filerecord['mimetype'])) {
1338             $filerecord['mimetype'] = $imageinfo['mimetype'];
1339         }
1341         $width    = $imageinfo['width'];
1342         $height   = $imageinfo['height'];
1343         $mimetype = $imageinfo['mimetype'];
1345         if ($keepaspectratio) {
1346             if (0 >= $newwidth and 0 >= $newheight) {
1347                 // no sizes specified
1348                 $newwidth  = $width;
1349                 $newheight = $height;
1351             } else if (0 < $newwidth and 0 < $newheight) {
1352                 $xheight = ($newwidth*($height/$width));
1353                 if ($xheight < $newheight) {
1354                     $newheight = (int)$xheight;
1355                 } else {
1356                     $newwidth = (int)($newheight*($width/$height));
1357                 }
1359             } else if (0 < $newwidth) {
1360                 $newheight = (int)($newwidth*($height/$width));
1362             } else { //0 < $newheight
1363                 $newwidth = (int)($newheight*($width/$height));
1364             }
1366         } else {
1367             if (0 >= $newwidth) {
1368                 $newwidth = $width;
1369             }
1370             if (0 >= $newheight) {
1371                 $newheight = $height;
1372             }
1373         }
1375         $img = imagecreatefromstring($file->get_content());
1376         if ($height != $newheight or $width != $newwidth) {
1377             $newimg = imagecreatetruecolor($newwidth, $newheight);
1378             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1379                 // weird
1380                 throw new file_exception('storedfileproblem', 'Can not resize image');
1381             }
1382             imagedestroy($img);
1383             $img = $newimg;
1384         }
1386         ob_start();
1387         switch ($filerecord['mimetype']) {
1388             case 'image/gif':
1389                 imagegif($img);
1390                 break;
1392             case 'image/jpeg':
1393                 if (is_null($quality)) {
1394                     imagejpeg($img);
1395                 } else {
1396                     imagejpeg($img, NULL, $quality);
1397                 }
1398                 break;
1400             case 'image/png':
1401                 $quality = (int)$quality;
1402                 imagepng($img, NULL, $quality, NULL);
1403                 break;
1405             default:
1406                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1407         }
1409         $content = ob_get_contents();
1410         ob_end_clean();
1411         imagedestroy($img);
1413         if (!$content) {
1414             throw new file_exception('storedfileproblem', 'Can not convert image');
1415         }
1417         return $this->create_file_from_string($filerecord, $content);
1418     }
1420     /**
1421      * Add file content to sha1 pool.
1422      *
1423      * @param string $pathname path to file
1424      * @param string $contenthash sha1 hash of content if known (performance only)
1425      * @return array (contenthash, filesize, newfile)
1426      */
1427     public function add_file_to_pool($pathname, $contenthash = NULL) {
1428         if (!is_readable($pathname)) {
1429             throw new file_exception('storedfilecannotread', '', $pathname);
1430         }
1432         if (is_null($contenthash)) {
1433             $contenthash = sha1_file($pathname);
1434         }
1436         $filesize = filesize($pathname);
1438         $hashpath = $this->path_from_hash($contenthash);
1439         $hashfile = "$hashpath/$contenthash";
1441         if (file_exists($hashfile)) {
1442             if (filesize($hashfile) !== $filesize) {
1443                 throw new file_pool_content_exception($contenthash);
1444             }
1445             $newfile = false;
1447         } else {
1448             if (!is_dir($hashpath)) {
1449                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1450                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1451                 }
1452             }
1453             $newfile = true;
1455             if (!copy($pathname, $hashfile)) {
1456                 throw new file_exception('storedfilecannotread', '', $pathname);
1457             }
1459             if (filesize($hashfile) !== $filesize) {
1460                 @unlink($hashfile);
1461                 throw new file_pool_content_exception($contenthash);
1462             }
1463             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1464         }
1467         return array($contenthash, $filesize, $newfile);
1468     }
1470     /**
1471      * Add string content to sha1 pool.
1472      *
1473      * @param string $content file content - binary string
1474      * @return array (contenthash, filesize, newfile)
1475      */
1476     public function add_string_to_pool($content) {
1477         $contenthash = sha1($content);
1478         $filesize = strlen($content); // binary length
1480         $hashpath = $this->path_from_hash($contenthash);
1481         $hashfile = "$hashpath/$contenthash";
1484         if (file_exists($hashfile)) {
1485             if (filesize($hashfile) !== $filesize) {
1486                 throw new file_pool_content_exception($contenthash);
1487             }
1488             $newfile = false;
1490         } else {
1491             if (!is_dir($hashpath)) {
1492                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1493                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1494                 }
1495             }
1496             $newfile = true;
1498             file_put_contents($hashfile, $content);
1500             if (filesize($hashfile) !== $filesize) {
1501                 @unlink($hashfile);
1502                 throw new file_pool_content_exception($contenthash);
1503             }
1504             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1505         }
1507         return array($contenthash, $filesize, $newfile);
1508     }
1510     /**
1511      * Serve file content using X-Sendfile header.
1512      * Please make sure that all headers are already sent
1513      * and the all access control checks passed.
1514      *
1515      * @param string $contenthash sah1 hash of the file content to be served
1516      * @return bool success
1517      */
1518     public function xsendfile($contenthash) {
1519         global $CFG;
1520         require_once("$CFG->libdir/xsendfilelib.php");
1522         $hashpath = $this->path_from_hash($contenthash);
1523         return xsendfile("$hashpath/$contenthash");
1524     }
1526     /**
1527      * Content exists
1528      *
1529      * @param string $contenthash
1530      * @return bool
1531      */
1532     public function content_exists($contenthash) {
1533         $dir = $this->path_from_hash($contenthash);
1534         $filepath = $dir . '/' . $contenthash;
1535         return file_exists($filepath);
1536     }
1538     /**
1539      * Return path to file with given hash.
1540      *
1541      * NOTE: must not be public, files in pool must not be modified
1542      *
1543      * @param string $contenthash content hash
1544      * @return string expected file location
1545      */
1546     protected function path_from_hash($contenthash) {
1547         $l1 = $contenthash[0].$contenthash[1];
1548         $l2 = $contenthash[2].$contenthash[3];
1549         return "$this->filedir/$l1/$l2";
1550     }
1552     /**
1553      * Return path to file with given hash.
1554      *
1555      * NOTE: must not be public, files in pool must not be modified
1556      *
1557      * @param string $contenthash content hash
1558      * @return string expected file location
1559      */
1560     protected function trash_path_from_hash($contenthash) {
1561         $l1 = $contenthash[0].$contenthash[1];
1562         $l2 = $contenthash[2].$contenthash[3];
1563         return "$this->trashdir/$l1/$l2";
1564     }
1566     /**
1567      * Tries to recover missing content of file from trash.
1568      *
1569      * @param stored_file $file stored_file instance
1570      * @return bool success
1571      */
1572     public function try_content_recovery($file) {
1573         $contenthash = $file->get_contenthash();
1574         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1575         if (!is_readable($trashfile)) {
1576             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1577                 return false;
1578             }
1579             // nice, at least alternative trash file in trash root exists
1580             $trashfile = $this->trashdir.'/'.$contenthash;
1581         }
1582         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1583             //weird, better fail early
1584             return false;
1585         }
1586         $contentdir  = $this->path_from_hash($contenthash);
1587         $contentfile = $contentdir.'/'.$contenthash;
1588         if (file_exists($contentfile)) {
1589             //strange, no need to recover anything
1590             return true;
1591         }
1592         if (!is_dir($contentdir)) {
1593             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1594                 return false;
1595             }
1596         }
1597         return rename($trashfile, $contentfile);
1598     }
1600     /**
1601      * Marks pool file as candidate for deleting.
1602      *
1603      * DO NOT call directly - reserved for core!!
1604      *
1605      * @param string $contenthash
1606      */
1607     public function deleted_file_cleanup($contenthash) {
1608         global $DB;
1610         //Note: this section is critical - in theory file could be reused at the same
1611         //      time, if this happens we can still recover the file from trash
1612         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1613             // file content is still used
1614             return;
1615         }
1616         //move content file to trash
1617         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1618         if (!file_exists($contentfile)) {
1619             //weird, but no problem
1620             return;
1621         }
1622         $trashpath = $this->trash_path_from_hash($contenthash);
1623         $trashfile = $trashpath.'/'.$contenthash;
1624         if (file_exists($trashfile)) {
1625             // we already have this content in trash, no need to move it there
1626             unlink($contentfile);
1627             return;
1628         }
1629         if (!is_dir($trashpath)) {
1630             mkdir($trashpath, $this->dirpermissions, true);
1631         }
1632         rename($contentfile, $trashfile);
1633         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1634     }
1636     /**
1637      * When user referring to a moodle file, we build the reference field
1638      *
1639      * @param array $params
1640      * @return string
1641      */
1642     public static function pack_reference($params) {
1643         $params = (array)$params;
1644         $reference = array();
1645         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1646         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1647         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1648         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1649         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);;
1650         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1651         return base64_encode(serialize($reference));
1652     }
1654     /**
1655      * Unpack reference field
1656      *
1657      * @param string $str
1658      * @return array
1659      */
1660     public static function unpack_reference($str) {
1661         return unserialize(base64_decode($str));
1662     }
1664     /**
1665      * Search references by providing reference content
1666      *
1667      * @param string $str
1668      * @return array
1669      */
1670     public function search_references($str) {
1671         global $DB;
1672         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1673                   FROM {files} f
1674              LEFT JOIN {files_reference} r
1675                        ON f.referencefileid = r.id
1676                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1677                  AND (f.component <> ? OR f.filearea <> ?)";
1679         $rs = $DB->get_recordset_sql($sql, array($str, 'user', 'draft'));
1680         $files = array();
1681         foreach ($rs as $filerecord) {
1682             $file = $this->get_file_instance($filerecord);
1683             if ($file->is_external_file()) {
1684                 $files[$filerecord->pathnamehash] = $file;
1685             }
1686         }
1688         return $files;
1689     }
1691     /**
1692      * Search references count by providing reference content
1693      *
1694      * @param string $str
1695      * @return int
1696      */
1697     public function search_references_count($str) {
1698         global $DB;
1699         $sql = "SELECT COUNT(f.id)
1700                   FROM {files} f
1701              LEFT JOIN {files_reference} r
1702                        ON f.referencefileid = r.id
1703                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1704                  AND (f.component <> ? OR f.filearea <> ?)";
1706         $count = $DB->count_records_sql($sql, array($str, 'user', 'draft'));
1707         return $count;
1708     }
1710     /**
1711      * Return all files referring to provided stored_file instance
1712      * This won't work for draft files
1713      *
1714      * @param stored_file $storedfile
1715      * @return array
1716      */
1717     public function get_references_by_storedfile($storedfile) {
1718         global $DB;
1720         $params = array();
1721         $params['contextid'] = $storedfile->get_contextid();
1722         $params['component'] = $storedfile->get_component();
1723         $params['filearea']  = $storedfile->get_filearea();
1724         $params['itemid']    = $storedfile->get_itemid();
1725         $params['filename']  = $storedfile->get_filename();
1726         $params['filepath']  = $storedfile->get_filepath();
1727         $params['userid']    = $storedfile->get_userid();
1729         $reference = self::pack_reference($params);
1731         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1732                   FROM {files} f
1733              LEFT JOIN {files_reference} r
1734                        ON f.referencefileid = r.id
1735                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1736                    AND (f.component <> ? OR f.filearea <> ?)";
1738         $rs = $DB->get_recordset_sql($sql, array($reference, 'user', 'draft'));
1739         $files = array();
1740         foreach ($rs as $filerecord) {
1741             $file = $this->get_file_instance($filerecord);
1742             if ($file->is_external_file()) {
1743                 $files[$filerecord->pathnamehash] = $file;
1744             }
1745         }
1747         return $files;
1748     }
1750     /**
1751      * Return the count files referring to provided stored_file instance
1752      * This won't work for draft files
1753      *
1754      * @param stored_file $storedfile
1755      * @return int
1756      */
1757     public function get_references_count_by_storedfile($storedfile) {
1758         global $DB;
1760         $params = array();
1761         $params['contextid'] = $storedfile->get_contextid();
1762         $params['component'] = $storedfile->get_component();
1763         $params['filearea']  = $storedfile->get_filearea();
1764         $params['itemid']    = $storedfile->get_itemid();
1765         $params['filename']  = $storedfile->get_filename();
1766         $params['filepath']  = $storedfile->get_filepath();
1767         $params['userid']    = $storedfile->get_userid();
1769         $reference = self::pack_reference($params);
1771         $sql = "SELECT COUNT(f.id)
1772                   FROM {files} f
1773              LEFT JOIN {files_reference} r
1774                        ON f.referencefileid = r.id
1775                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1776                  AND (f.component <> ? OR f.filearea <> ?)";
1778         $count = $DB->count_records_sql($sql, array($reference, 'user', 'draft'));
1779         return $count;
1780     }
1782     /**
1783      * Convert file alias to local file
1784      *
1785      * @param stored_file $storedfile a stored_file instances
1786      * @return stored_file stored_file
1787      */
1788     public function import_external_file($storedfile) {
1789         global $CFG;
1790         require_once($CFG->dirroot.'/repository/lib.php');
1791         // sync external file
1792         repository::sync_external_file($storedfile);
1793         // Remove file references
1794         $storedfile->delete_reference();
1795         return $storedfile;
1796     }
1798     /**
1799      * Return mimetype by given file pathname
1800      *
1801      * If file has a known extension, we return the mimetype based on extension.
1802      * Otherwise (when possible) we try to get the mimetype from file contents.
1803      *
1804      * @param string $pathname full path to the file
1805      * @param string $filename correct file name with extension, if omitted will be taken from $path
1806      * @return string
1807      */
1808     public static function mimetype($pathname, $filename = null) {
1809         if (empty($filename)) {
1810             $filename = $pathname;
1811         }
1812         $type = mimeinfo('type', $filename);
1813         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1814             $finfo = new finfo(FILEINFO_MIME_TYPE);
1815             $type = mimeinfo_from_type('type', $finfo->file($pathname));
1816         }
1817         return $type;
1818     }
1820     /**
1821      * Cron cleanup job.
1822      */
1823     public function cron() {
1824         global $CFG, $DB;
1826         // find out all stale draft areas (older than 4 days) and purge them
1827         // those are identified by time stamp of the /. root dir
1828         mtrace('Deleting old draft files... ', '');
1829         $old = time() - 60*60*24*4;
1830         $sql = "SELECT *
1831                   FROM {files}
1832                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1833                        AND timecreated < :old";
1834         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1835         foreach ($rs as $dir) {
1836             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1837         }
1838         $rs->close();
1839         mtrace('done.');
1841         // remove orphaned preview files (that is files in the core preview filearea without
1842         // the existing original file)
1843         mtrace('Deleting orphaned preview files... ', '');
1844         $sql = "SELECT p.*
1845                   FROM {files} p
1846              LEFT JOIN {files} o ON (p.filename = o.contenthash)
1847                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
1848                        AND o.id IS NULL";
1849         $syscontext = context_system::instance();
1850         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
1851         foreach ($rs as $orphan) {
1852             $file = $this->get_file_instance($orphan);
1853             if (!$file->is_directory()) {
1854                 $file->delete();
1855             }
1856         }
1857         $rs->close();
1858         mtrace('done.');
1860         // remove trash pool files once a day
1861         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1862         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1863             require_once($CFG->libdir.'/filelib.php');
1864             // Delete files that are associated with a context that no longer exists.
1865             mtrace('Cleaning up files from deleted contexts... ', '');
1866             $sql = "SELECT DISTINCT f.contextid
1867                     FROM {files} f
1868                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
1869                     WHERE c.id IS NULL";
1870             $rs = $DB->get_recordset_sql($sql);
1871             if ($rs->valid()) {
1872                 $fs = get_file_storage();
1873                 foreach ($rs as $ctx) {
1874                     $fs->delete_area_files($ctx->contextid);
1875                 }
1876             }
1877             $rs->close();
1878             mtrace('done.');
1880             mtrace('Deleting trash files... ', '');
1881             fulldelete($this->trashdir);
1882             set_config('fileslastcleanup', time());
1883             mtrace('done.');
1884         }
1885     }
1887     /**
1888      * Get the sql formated fields for a file instance to be created from a
1889      * {files} and {files_refernece} join.
1890      *
1891      * @param string $filesprefix the table prefix for the {files} table
1892      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
1893      * @return string the sql to go after a SELECT
1894      */
1895     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
1896         // Note, these fieldnames MUST NOT overlap between the two tables,
1897         // else problems like MDL-33172 occur.
1898         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
1899             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
1900             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid',
1901             'referencelastsync', 'referencelifetime');
1903         $referencefields = array('repositoryid', 'reference');
1905         // id is specifically named to prevent overlaping between the two tables.
1906         $fields = array();
1907         $fields[] = $filesprefix.'.id AS id';
1908         foreach ($filefields as $field) {
1909             $fields[] = "{$filesprefix}.{$field}";
1910         }
1912         foreach ($referencefields as $field) {
1913             $fields[] = "{$filesreferenceprefix}.{$field}";
1914         }
1916         return implode(', ', $fields);
1917     }