Merge branch 'wip-MDL-42089-master' of git://github.com/abgreeve/moodle
[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         global $CFG;
69         $this->filedir         = $filedir;
70         $this->trashdir        = $trashdir;
71         $this->tempdir         = $tempdir;
72         $this->dirpermissions  = $dirpermissions;
73         $this->filepermissions = $filepermissions;
75         // make sure the file pool directory exists
76         if (!is_dir($this->filedir)) {
77             if (!mkdir($this->filedir, $this->dirpermissions, true)) {
78                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
79             }
80             // place warning file in file pool root
81             if (!file_exists($this->filedir.'/warning.txt')) {
82                 file_put_contents($this->filedir.'/warning.txt',
83                                   'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
84                 chmod($this->filedir.'/warning.txt', $CFG->filepermissions);
85             }
86         }
87         // make sure the file pool directory exists
88         if (!is_dir($this->trashdir)) {
89             if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
90                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
91             }
92         }
93     }
95     /**
96      * Calculates sha1 hash of unique full path name information.
97      *
98      * This hash is a unique file identifier - it is used to improve
99      * performance and overcome db index size limits.
100      *
101      * @param int $contextid context ID
102      * @param string $component component
103      * @param string $filearea file area
104      * @param int $itemid item ID
105      * @param string $filepath file path
106      * @param string $filename file name
107      * @return string sha1 hash
108      */
109     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
110         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
111     }
113     /**
114      * Does this file exist?
115      *
116      * @param int $contextid context ID
117      * @param string $component component
118      * @param string $filearea file area
119      * @param int $itemid item ID
120      * @param string $filepath file path
121      * @param string $filename file name
122      * @return bool
123      */
124     public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
125         $filepath = clean_param($filepath, PARAM_PATH);
126         $filename = clean_param($filename, PARAM_FILE);
128         if ($filename === '') {
129             $filename = '.';
130         }
132         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
133         return $this->file_exists_by_hash($pathnamehash);
134     }
136     /**
137      * Whether or not the file exist
138      *
139      * @param string $pathnamehash path name hash
140      * @return bool
141      */
142     public function file_exists_by_hash($pathnamehash) {
143         global $DB;
145         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
146     }
148     /**
149      * Create instance of file class from database record.
150      *
151      * @param stdClass $filerecord record from the files table left join files_reference table
152      * @return stored_file instance of file abstraction class
153      */
154     public function get_file_instance(stdClass $filerecord) {
155         $storedfile = new stored_file($this, $filerecord, $this->filedir);
156         return $storedfile;
157     }
159     /**
160      * Returns an image file that represent the given stored file as a preview
161      *
162      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
163      * future, the support for other mimetypes can be added, too (eg. generate an image
164      * preview of PDF, text documents etc).
165      *
166      * @param stored_file $file the file we want to preview
167      * @param string $mode preview mode, eg. 'thumb'
168      * @return stored_file|bool false if unable to create the preview, stored file otherwise
169      */
170     public function get_file_preview(stored_file $file, $mode) {
172         $context = context_system::instance();
173         $path = '/' . trim($mode, '/') . '/';
174         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
176         if (!$preview) {
177             $preview = $this->create_file_preview($file, $mode);
178             if (!$preview) {
179                 return false;
180             }
181         }
183         return $preview;
184     }
186     /**
187      * Return an available file name.
188      *
189      * This will return the next available file name in the area, adding/incrementing a suffix
190      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
191      *
192      * If the file name passed is available without modification, it is returned as is.
193      *
194      * @param int $contextid context ID.
195      * @param string $component component.
196      * @param string $filearea file area.
197      * @param int $itemid area item ID.
198      * @param string $filepath the file path.
199      * @param string $filename the file name.
200      * @return string available file name.
201      * @throws coding_exception if the file name is invalid.
202      * @since 2.5
203      */
204     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
205         global $DB;
207         // Do not accept '.' or an empty file name (zero is acceptable).
208         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
209             throw new coding_exception('Invalid file name passed', $filename);
210         }
212         // The file does not exist, we return the same file name.
213         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
214             return $filename;
215         }
217         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
218         $pathinfo = pathinfo($filename);
219         $basename = $pathinfo['filename'];
220         $matches = array();
221         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
222             $basename = $matches[1];
223         }
225         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
226         if (isset($pathinfo['extension'])) {
227             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
228         }
230         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
231         $filenamelen = $DB->sql_length('f.filename');
232         $sql = "SELECT filename
233                 FROM {files} f
234                 WHERE
235                     f.contextid = :contextid AND
236                     f.component = :component AND
237                     f.filearea = :filearea AND
238                     f.itemid = :itemid AND
239                     f.filepath = :filepath AND
240                     $filenamelikesql
241                 ORDER BY
242                     $filenamelen DESC,
243                     f.filename DESC";
244         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
245                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
246         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
248         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
249         // would both be returned, but only the one only containing digits should be used.
250         $number = 1;
251         foreach ($results as $result) {
252             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
253             $matches = array();
254             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
255                 $number = $matches[2] + 1;
256                 break;
257             }
258         }
260         // Constructing the new filename.
261         $newfilename = $basename . ' (' . $number . ')';
262         if (isset($pathinfo['extension'])) {
263             $newfilename .= '.' . $pathinfo['extension'];
264         }
266         return $newfilename;
267     }
269     /**
270      * Return an available directory name.
271      *
272      * This will return the next available directory name in the area, adding/incrementing a suffix
273      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
274      *
275      * If the file path passed is available without modification, it is returned as is.
276      *
277      * @param int $contextid context ID.
278      * @param string $component component.
279      * @param string $filearea file area.
280      * @param int $itemid area item ID.
281      * @param string $suggestedpath the suggested file path.
282      * @return string available file path
283      * @since 2.5
284      */
285     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
286         global $DB;
288         // Ensure suggestedpath has trailing '/'
289         $suggestedpath = rtrim($suggestedpath, '/'). '/';
291         // The directory does not exist, we return the same file path.
292         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
293             return $suggestedpath;
294         }
296         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
297         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
298             $suggestedpath = $matches[1]. '/';
299         }
301         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
303         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
304         $filepathlen = $DB->sql_length('f.filepath');
305         $sql = "SELECT filepath
306                 FROM {files} f
307                 WHERE
308                     f.contextid = :contextid AND
309                     f.component = :component AND
310                     f.filearea = :filearea AND
311                     f.itemid = :itemid AND
312                     f.filename = :filename AND
313                     $filepathlikesql
314                 ORDER BY
315                     $filepathlen DESC,
316                     f.filepath DESC";
317         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
318                 'filename' => '.', 'filepathlike' => $filepathlike);
319         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
321         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
322         // would both be returned, but only the one only containing digits should be used.
323         $number = 1;
324         foreach ($results as $result) {
325             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
326                 $number = (int)($matches[1]) + 1;
327                 break;
328             }
329         }
331         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
332     }
334     /**
335      * Generates a preview image for the stored file
336      *
337      * @param stored_file $file the file we want to preview
338      * @param string $mode preview mode, eg. 'thumb'
339      * @return stored_file|bool the newly created preview file or false
340      */
341     protected function create_file_preview(stored_file $file, $mode) {
343         $mimetype = $file->get_mimetype();
345         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
346             // make a preview of the image
347             $data = $this->create_imagefile_preview($file, $mode);
349         } else {
350             // unable to create the preview of this mimetype yet
351             return false;
352         }
354         if (empty($data)) {
355             return false;
356         }
358         // getimagesizefromstring() is available from PHP 5.4 but we need to support
359         // lower versions, so...
360         $tmproot = make_temp_directory('thumbnails');
361         $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
362         file_put_contents($tmpfilepath, $data);
363         $imageinfo = getimagesize($tmpfilepath);
364         unlink($tmpfilepath);
366         $context = context_system::instance();
368         $record = array(
369             'contextid' => $context->id,
370             'component' => 'core',
371             'filearea'  => 'preview',
372             'itemid'    => 0,
373             'filepath'  => '/' . trim($mode, '/') . '/',
374             'filename'  => $file->get_contenthash(),
375         );
377         if ($imageinfo) {
378             $record['mimetype'] = $imageinfo['mime'];
379         }
381         return $this->create_file_from_string($record, $data);
382     }
384     /**
385      * Generates a preview for the stored image file
386      *
387      * @param stored_file $file the image we want to preview
388      * @param string $mode preview mode, eg. 'thumb'
389      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
390      */
391     protected function create_imagefile_preview(stored_file $file, $mode) {
392         global $CFG;
393         require_once($CFG->libdir.'/gdlib.php');
395         $tmproot = make_temp_directory('thumbnails');
396         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
397         $file->copy_content_to($tmpfilepath);
399         if ($mode === 'tinyicon') {
400             $data = generate_image_thumbnail($tmpfilepath, 24, 24);
402         } else if ($mode === 'thumb') {
403             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
405         } else if ($mode === 'bigthumb') {
406             $data = generate_image_thumbnail($tmpfilepath, 250, 250);
408         } else {
409             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
410         }
412         unlink($tmpfilepath);
414         return $data;
415     }
417     /**
418      * Fetch file using local file id.
419      *
420      * Please do not rely on file ids, it is usually easier to use
421      * pathname hashes instead.
422      *
423      * @param int $fileid file ID
424      * @return stored_file|bool stored_file instance if exists, false if not
425      */
426     public function get_file_by_id($fileid) {
427         global $DB;
429         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
430                   FROM {files} f
431              LEFT JOIN {files_reference} r
432                        ON f.referencefileid = r.id
433                  WHERE f.id = ?";
434         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
435             return $this->get_file_instance($filerecord);
436         } else {
437             return false;
438         }
439     }
441     /**
442      * Fetch file using local file full pathname hash
443      *
444      * @param string $pathnamehash path name hash
445      * @return stored_file|bool stored_file instance if exists, false if not
446      */
447     public function get_file_by_hash($pathnamehash) {
448         global $DB;
450         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
451                   FROM {files} f
452              LEFT JOIN {files_reference} r
453                        ON f.referencefileid = r.id
454                  WHERE f.pathnamehash = ?";
455         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
456             return $this->get_file_instance($filerecord);
457         } else {
458             return false;
459         }
460     }
462     /**
463      * Fetch locally stored file.
464      *
465      * @param int $contextid context ID
466      * @param string $component component
467      * @param string $filearea file area
468      * @param int $itemid item ID
469      * @param string $filepath file path
470      * @param string $filename file name
471      * @return stored_file|bool stored_file instance if exists, false if not
472      */
473     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
474         $filepath = clean_param($filepath, PARAM_PATH);
475         $filename = clean_param($filename, PARAM_FILE);
477         if ($filename === '') {
478             $filename = '.';
479         }
481         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
482         return $this->get_file_by_hash($pathnamehash);
483     }
485     /**
486      * Are there any files (or directories)
487      *
488      * @param int $contextid context ID
489      * @param string $component component
490      * @param string $filearea file area
491      * @param bool|int $itemid item id or false if all items
492      * @param bool $ignoredirs whether or not ignore directories
493      * @return bool empty
494      */
495     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
496         global $DB;
498         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
499         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
501         if ($itemid !== false) {
502             $params['itemid'] = $itemid;
503             $where .= " AND itemid = :itemid";
504         }
506         if ($ignoredirs) {
507             $sql = "SELECT 'x'
508                       FROM {files}
509                      WHERE $where AND filename <> '.'";
510         } else {
511             $sql = "SELECT 'x'
512                       FROM {files}
513                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
514         }
516         return !$DB->record_exists_sql($sql, $params);
517     }
519     /**
520      * Returns all files belonging to given repository
521      *
522      * @param int $repositoryid
523      * @param string $sort A fragment of SQL to use for sorting
524      */
525     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
526         global $DB;
527         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
528                   FROM {files} f
529              LEFT JOIN {files_reference} r
530                        ON f.referencefileid = r.id
531                  WHERE r.repositoryid = ?";
532         if (!empty($sort)) {
533             $sql .= " ORDER BY {$sort}";
534         }
536         $result = array();
537         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
538         foreach ($filerecords as $filerecord) {
539             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
540         }
541         return $result;
542     }
544     /**
545      * Returns all area files (optionally limited by itemid)
546      *
547      * @param int $contextid context ID
548      * @param string $component component
549      * @param string $filearea file area
550      * @param int $itemid item ID or all files if not specified
551      * @param string $sort A fragment of SQL to use for sorting
552      * @param bool $includedirs whether or not include directories
553      * @return stored_file[] array of stored_files indexed by pathanmehash
554      */
555     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
556         global $DB;
558         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
559         if ($itemid !== false) {
560             $itemidsql = ' AND f.itemid = :itemid ';
561             $conditions['itemid'] = $itemid;
562         } else {
563             $itemidsql = '';
564         }
566         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
567                   FROM {files} f
568              LEFT JOIN {files_reference} r
569                        ON f.referencefileid = r.id
570                  WHERE f.contextid = :contextid
571                        AND f.component = :component
572                        AND f.filearea = :filearea
573                        $itemidsql";
574         if (!empty($sort)) {
575             $sql .= " ORDER BY {$sort}";
576         }
578         $result = array();
579         $filerecords = $DB->get_records_sql($sql, $conditions);
580         foreach ($filerecords as $filerecord) {
581             if (!$includedirs and $filerecord->filename === '.') {
582                 continue;
583             }
584             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
585         }
586         return $result;
587     }
589     /**
590      * Returns array based tree structure of area files
591      *
592      * @param int $contextid context ID
593      * @param string $component component
594      * @param string $filearea file area
595      * @param int $itemid item ID
596      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
597      */
598     public function get_area_tree($contextid, $component, $filearea, $itemid) {
599         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
600         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
601         // first create directory structure
602         foreach ($files as $hash=>$dir) {
603             if (!$dir->is_directory()) {
604                 continue;
605             }
606             unset($files[$hash]);
607             if ($dir->get_filepath() === '/') {
608                 $result['dirfile'] = $dir;
609                 continue;
610             }
611             $parts = explode('/', trim($dir->get_filepath(),'/'));
612             $pointer =& $result;
613             foreach ($parts as $part) {
614                 if ($part === '') {
615                     continue;
616                 }
617                 if (!isset($pointer['subdirs'][$part])) {
618                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
619                 }
620                 $pointer =& $pointer['subdirs'][$part];
621             }
622             $pointer['dirfile'] = $dir;
623             unset($pointer);
624         }
625         foreach ($files as $hash=>$file) {
626             $parts = explode('/', trim($file->get_filepath(),'/'));
627             $pointer =& $result;
628             foreach ($parts as $part) {
629                 if ($part === '') {
630                     continue;
631                 }
632                 $pointer =& $pointer['subdirs'][$part];
633             }
634             $pointer['files'][$file->get_filename()] = $file;
635             unset($pointer);
636         }
637         $result = $this->sort_area_tree($result);
638         return $result;
639     }
641     /**
642      * Sorts the result of {@link file_storage::get_area_tree()}.
643      *
644      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
645      * @return array of sorted results
646      */
647     protected function sort_area_tree($tree) {
648         foreach ($tree as $key => &$value) {
649             if ($key == 'subdirs') {
650                 core_collator::ksort($value, core_collator::SORT_NATURAL);
651                 foreach ($value as $subdirname => &$subtree) {
652                     $subtree = $this->sort_area_tree($subtree);
653                 }
654             } else if ($key == 'files') {
655                 core_collator::ksort($value, core_collator::SORT_NATURAL);
656             }
657         }
658         return $tree;
659     }
661     /**
662      * Returns all files and optionally directories
663      *
664      * @param int $contextid context ID
665      * @param string $component component
666      * @param string $filearea file area
667      * @param int $itemid item ID
668      * @param int $filepath directory path
669      * @param bool $recursive include all subdirectories
670      * @param bool $includedirs include files and directories
671      * @param string $sort A fragment of SQL to use for sorting
672      * @return array of stored_files indexed by pathanmehash
673      */
674     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
675         global $DB;
677         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
678             return array();
679         }
681         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
683         if ($recursive) {
685             $dirs = $includedirs ? "" : "AND filename <> '.'";
686             $length = core_text::strlen($filepath);
688             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
689                       FROM {files} f
690                  LEFT JOIN {files_reference} r
691                            ON f.referencefileid = r.id
692                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
693                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
694                            AND f.id <> :dirid
695                            $dirs
696                            $orderby";
697             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
699             $files = array();
700             $dirs  = array();
701             $filerecords = $DB->get_records_sql($sql, $params);
702             foreach ($filerecords as $filerecord) {
703                 if ($filerecord->filename == '.') {
704                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
705                 } else {
706                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
707                 }
708             }
709             $result = array_merge($dirs, $files);
711         } else {
712             $result = array();
713             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
715             $length = core_text::strlen($filepath);
717             if ($includedirs) {
718                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
719                           FROM {files} f
720                      LEFT JOIN {files_reference} r
721                                ON f.referencefileid = r.id
722                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
723                                AND f.itemid = :itemid AND f.filename = '.'
724                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
725                                AND f.id <> :dirid
726                                $orderby";
727                 $reqlevel = substr_count($filepath, '/') + 1;
728                 $filerecords = $DB->get_records_sql($sql, $params);
729                 foreach ($filerecords as $filerecord) {
730                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
731                         continue;
732                     }
733                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
734                 }
735             }
737             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
738                       FROM {files} f
739                  LEFT JOIN {files_reference} r
740                            ON f.referencefileid = r.id
741                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
742                            AND f.filepath = :filepath AND f.filename <> '.'
743                            $orderby";
745             $filerecords = $DB->get_records_sql($sql, $params);
746             foreach ($filerecords as $filerecord) {
747                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
748             }
749         }
751         return $result;
752     }
754     /**
755      * Delete all area files (optionally limited by itemid).
756      *
757      * @param int $contextid context ID
758      * @param string $component component
759      * @param string $filearea file area or all areas in context if not specified
760      * @param int $itemid item ID or all files if not specified
761      * @return bool success
762      */
763     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
764         global $DB;
766         $conditions = array('contextid'=>$contextid);
767         if ($component !== false) {
768             $conditions['component'] = $component;
769         }
770         if ($filearea !== false) {
771             $conditions['filearea'] = $filearea;
772         }
773         if ($itemid !== false) {
774             $conditions['itemid'] = $itemid;
775         }
777         $filerecords = $DB->get_records('files', $conditions);
778         foreach ($filerecords as $filerecord) {
779             $this->get_file_instance($filerecord)->delete();
780         }
782         return true; // BC only
783     }
785     /**
786      * Delete all the files from certain areas where itemid is limited by an
787      * arbitrary bit of SQL.
788      *
789      * @param int $contextid the id of the context the files belong to. Must be given.
790      * @param string $component the owning component. Must be given.
791      * @param string $filearea the file area name. Must be given.
792      * @param string $itemidstest an SQL fragment that the itemid must match. Used
793      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
794      *      and may not used named parameters called contextid, component or filearea.
795      * @param array $params any query params used by $itemidstest.
796      */
797     public function delete_area_files_select($contextid, $component,
798             $filearea, $itemidstest, array $params = null) {
799         global $DB;
801         $where = "contextid = :contextid
802                 AND component = :component
803                 AND filearea = :filearea
804                 AND itemid $itemidstest";
805         $params['contextid'] = $contextid;
806         $params['component'] = $component;
807         $params['filearea'] = $filearea;
809         $filerecords = $DB->get_recordset_select('files', $where, $params);
810         foreach ($filerecords as $filerecord) {
811             $this->get_file_instance($filerecord)->delete();
812         }
813         $filerecords->close();
814     }
816     /**
817      * Delete all files associated with the given component.
818      *
819      * @param string $component the component owning the file
820      */
821     public function delete_component_files($component) {
822         global $DB;
824         $filerecords = $DB->get_recordset('files', array('component' => $component));
825         foreach ($filerecords as $filerecord) {
826             $this->get_file_instance($filerecord)->delete();
827         }
828         $filerecords->close();
829     }
831     /**
832      * Move all the files in a file area from one context to another.
833      *
834      * @param int $oldcontextid the context the files are being moved from.
835      * @param int $newcontextid the context the files are being moved to.
836      * @param string $component the plugin that these files belong to.
837      * @param string $filearea the name of the file area.
838      * @param int $itemid file item ID
839      * @return int the number of files moved, for information.
840      */
841     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
842         // Note, this code is based on some code that Petr wrote in
843         // forum_move_attachments in mod/forum/lib.php. I moved it here because
844         // I needed it in the question code too.
845         $count = 0;
847         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
848         foreach ($oldfiles as $oldfile) {
849             $filerecord = new stdClass();
850             $filerecord->contextid = $newcontextid;
851             $this->create_file_from_storedfile($filerecord, $oldfile);
852             $count += 1;
853         }
855         if ($count) {
856             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
857         }
859         return $count;
860     }
862     /**
863      * Recursively creates directory.
864      *
865      * @param int $contextid context ID
866      * @param string $component component
867      * @param string $filearea file area
868      * @param int $itemid item ID
869      * @param string $filepath file path
870      * @param int $userid the user ID
871      * @return bool success
872      */
873     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
874         global $DB;
876         // validate all parameters, we do not want any rubbish stored in database, right?
877         if (!is_number($contextid) or $contextid < 1) {
878             throw new file_exception('storedfileproblem', 'Invalid contextid');
879         }
881         $component = clean_param($component, PARAM_COMPONENT);
882         if (empty($component)) {
883             throw new file_exception('storedfileproblem', 'Invalid component');
884         }
886         $filearea = clean_param($filearea, PARAM_AREA);
887         if (empty($filearea)) {
888             throw new file_exception('storedfileproblem', 'Invalid filearea');
889         }
891         if (!is_number($itemid) or $itemid < 0) {
892             throw new file_exception('storedfileproblem', 'Invalid itemid');
893         }
895         $filepath = clean_param($filepath, PARAM_PATH);
896         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
897             // path must start and end with '/'
898             throw new file_exception('storedfileproblem', 'Invalid file path');
899         }
901         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
903         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
904             return $dir_info;
905         }
907         static $contenthash = null;
908         if (!$contenthash) {
909             $this->add_string_to_pool('');
910             $contenthash = sha1('');
911         }
913         $now = time();
915         $dir_record = new stdClass();
916         $dir_record->contextid = $contextid;
917         $dir_record->component = $component;
918         $dir_record->filearea  = $filearea;
919         $dir_record->itemid    = $itemid;
920         $dir_record->filepath  = $filepath;
921         $dir_record->filename  = '.';
922         $dir_record->contenthash  = $contenthash;
923         $dir_record->filesize  = 0;
925         $dir_record->timecreated  = $now;
926         $dir_record->timemodified = $now;
927         $dir_record->mimetype     = null;
928         $dir_record->userid       = $userid;
930         $dir_record->pathnamehash = $pathnamehash;
932         $DB->insert_record('files', $dir_record);
933         $dir_info = $this->get_file_by_hash($pathnamehash);
935         if ($filepath !== '/') {
936             //recurse to parent dirs
937             $filepath = trim($filepath, '/');
938             $filepath = explode('/', $filepath);
939             array_pop($filepath);
940             $filepath = implode('/', $filepath);
941             $filepath = ($filepath === '') ? '/' : "/$filepath/";
942             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
943         }
945         return $dir_info;
946     }
948     /**
949      * Add new local file based on existing local file.
950      *
951      * @param stdClass|array $filerecord object or array describing changes
952      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
953      * @return stored_file instance of newly created file
954      */
955     public function create_file_from_storedfile($filerecord, $fileorid) {
956         global $DB;
958         if ($fileorid instanceof stored_file) {
959             $fid = $fileorid->get_id();
960         } else {
961             $fid = $fileorid;
962         }
964         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
966         unset($filerecord['id']);
967         unset($filerecord['filesize']);
968         unset($filerecord['contenthash']);
969         unset($filerecord['pathnamehash']);
971         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
972                   FROM {files} f
973              LEFT JOIN {files_reference} r
974                        ON f.referencefileid = r.id
975                  WHERE f.id = ?";
977         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
978             throw new file_exception('storedfileproblem', 'File does not exist');
979         }
981         unset($newrecord->id);
983         foreach ($filerecord as $key => $value) {
984             // validate all parameters, we do not want any rubbish stored in database, right?
985             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
986                 throw new file_exception('storedfileproblem', 'Invalid contextid');
987             }
989             if ($key == 'component') {
990                 $value = clean_param($value, PARAM_COMPONENT);
991                 if (empty($value)) {
992                     throw new file_exception('storedfileproblem', 'Invalid component');
993                 }
994             }
996             if ($key == 'filearea') {
997                 $value = clean_param($value, PARAM_AREA);
998                 if (empty($value)) {
999                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1000                 }
1001             }
1003             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1004                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1005             }
1008             if ($key == 'filepath') {
1009                 $value = clean_param($value, PARAM_PATH);
1010                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1011                     // path must start and end with '/'
1012                     throw new file_exception('storedfileproblem', 'Invalid file path');
1013                 }
1014             }
1016             if ($key == 'filename') {
1017                 $value = clean_param($value, PARAM_FILE);
1018                 if ($value === '') {
1019                     // path must start and end with '/'
1020                     throw new file_exception('storedfileproblem', 'Invalid file name');
1021                 }
1022             }
1024             if ($key === 'timecreated' or $key === 'timemodified') {
1025                 if (!is_number($value)) {
1026                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1027                 }
1028                 if ($value < 0) {
1029                     //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)
1030                     $value = 0;
1031                 }
1032             }
1034             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
1035                 $value = clean_param($value, PARAM_INT);
1036             }
1038             $newrecord->$key = $value;
1039         }
1041         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1043         if ($newrecord->filename === '.') {
1044             // special case - only this function supports directories ;-)
1045             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1046             // update the existing directory with the new data
1047             $newrecord->id = $directory->get_id();
1048             $DB->update_record('files', $newrecord);
1049             return $this->get_file_instance($newrecord);
1050         }
1052         // note: referencefileid is copied from the original file so that
1053         // creating a new file from an existing alias creates new alias implicitly.
1054         // here we just check the database consistency.
1055         if (!empty($newrecord->repositoryid)) {
1056             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1057                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1058             }
1059         }
1061         try {
1062             $newrecord->id = $DB->insert_record('files', $newrecord);
1063         } catch (dml_exception $e) {
1064             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1065                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1066         }
1069         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1071         return $this->get_file_instance($newrecord);
1072     }
1074     /**
1075      * Add new local file.
1076      *
1077      * @param stdClass|array $filerecord object or array describing file
1078      * @param string $url the URL to the file
1079      * @param array $options {@link download_file_content()} options
1080      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1081      * @return stored_file
1082      */
1083     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1085         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1086         $filerecord = (object)$filerecord; // We support arrays too.
1088         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1089         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1090         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1091         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1092         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1093         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1094         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1096         if (!isset($filerecord->filename)) {
1097             $parts = explode('/', $url);
1098             $filename = array_pop($parts);
1099             $filerecord->filename = clean_param($filename, PARAM_FILE);
1100         }
1101         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1102         $filerecord->source = clean_param($source, PARAM_URL);
1104         if ($usetempfile) {
1105             check_dir_exists($this->tempdir);
1106             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1107             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1108             if ($content === false) {
1109                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1110             }
1111             try {
1112                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1113                 @unlink($tmpfile);
1114                 return $newfile;
1115             } catch (Exception $e) {
1116                 @unlink($tmpfile);
1117                 throw $e;
1118             }
1120         } else {
1121             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1122             if ($content === false) {
1123                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1124             }
1125             return $this->create_file_from_string($filerecord, $content);
1126         }
1127     }
1129     /**
1130      * Add new local file.
1131      *
1132      * @param stdClass|array $filerecord object or array describing file
1133      * @param string $pathname path to file or content of file
1134      * @return stored_file
1135      */
1136     public function create_file_from_pathname($filerecord, $pathname) {
1137         global $DB;
1139         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1140         $filerecord = (object)$filerecord; // We support arrays too.
1142         // validate all parameters, we do not want any rubbish stored in database, right?
1143         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1144             throw new file_exception('storedfileproblem', 'Invalid contextid');
1145         }
1147         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1148         if (empty($filerecord->component)) {
1149             throw new file_exception('storedfileproblem', 'Invalid component');
1150         }
1152         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1153         if (empty($filerecord->filearea)) {
1154             throw new file_exception('storedfileproblem', 'Invalid filearea');
1155         }
1157         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1158             throw new file_exception('storedfileproblem', 'Invalid itemid');
1159         }
1161         if (!empty($filerecord->sortorder)) {
1162             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1163                 $filerecord->sortorder = 0;
1164             }
1165         } else {
1166             $filerecord->sortorder = 0;
1167         }
1169         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1170         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1171             // path must start and end with '/'
1172             throw new file_exception('storedfileproblem', 'Invalid file path');
1173         }
1175         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1176         if ($filerecord->filename === '') {
1177             // filename must not be empty
1178             throw new file_exception('storedfileproblem', 'Invalid file name');
1179         }
1181         $now = time();
1182         if (isset($filerecord->timecreated)) {
1183             if (!is_number($filerecord->timecreated)) {
1184                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1185             }
1186             if ($filerecord->timecreated < 0) {
1187                 //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)
1188                 $filerecord->timecreated = 0;
1189             }
1190         } else {
1191             $filerecord->timecreated = $now;
1192         }
1194         if (isset($filerecord->timemodified)) {
1195             if (!is_number($filerecord->timemodified)) {
1196                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1197             }
1198             if ($filerecord->timemodified < 0) {
1199                 //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)
1200                 $filerecord->timemodified = 0;
1201             }
1202         } else {
1203             $filerecord->timemodified = $now;
1204         }
1206         $newrecord = new stdClass();
1208         $newrecord->contextid = $filerecord->contextid;
1209         $newrecord->component = $filerecord->component;
1210         $newrecord->filearea  = $filerecord->filearea;
1211         $newrecord->itemid    = $filerecord->itemid;
1212         $newrecord->filepath  = $filerecord->filepath;
1213         $newrecord->filename  = $filerecord->filename;
1215         $newrecord->timecreated  = $filerecord->timecreated;
1216         $newrecord->timemodified = $filerecord->timemodified;
1217         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1218         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1219         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1220         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1221         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1222         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1223         $newrecord->sortorder    = $filerecord->sortorder;
1225         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1227         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1229         try {
1230             $newrecord->id = $DB->insert_record('files', $newrecord);
1231         } catch (dml_exception $e) {
1232             if ($newfile) {
1233                 $this->deleted_file_cleanup($newrecord->contenthash);
1234             }
1235             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1236                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1237         }
1239         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1241         return $this->get_file_instance($newrecord);
1242     }
1244     /**
1245      * Add new local file.
1246      *
1247      * @param stdClass|array $filerecord object or array describing file
1248      * @param string $content content of file
1249      * @return stored_file
1250      */
1251     public function create_file_from_string($filerecord, $content) {
1252         global $DB;
1254         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1255         $filerecord = (object)$filerecord; // We support arrays too.
1257         // validate all parameters, we do not want any rubbish stored in database, right?
1258         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1259             throw new file_exception('storedfileproblem', 'Invalid contextid');
1260         }
1262         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1263         if (empty($filerecord->component)) {
1264             throw new file_exception('storedfileproblem', 'Invalid component');
1265         }
1267         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1268         if (empty($filerecord->filearea)) {
1269             throw new file_exception('storedfileproblem', 'Invalid filearea');
1270         }
1272         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1273             throw new file_exception('storedfileproblem', 'Invalid itemid');
1274         }
1276         if (!empty($filerecord->sortorder)) {
1277             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1278                 $filerecord->sortorder = 0;
1279             }
1280         } else {
1281             $filerecord->sortorder = 0;
1282         }
1284         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1285         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1286             // path must start and end with '/'
1287             throw new file_exception('storedfileproblem', 'Invalid file path');
1288         }
1290         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1291         if ($filerecord->filename === '') {
1292             // path must start and end with '/'
1293             throw new file_exception('storedfileproblem', 'Invalid file name');
1294         }
1296         $now = time();
1297         if (isset($filerecord->timecreated)) {
1298             if (!is_number($filerecord->timecreated)) {
1299                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1300             }
1301             if ($filerecord->timecreated < 0) {
1302                 //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)
1303                 $filerecord->timecreated = 0;
1304             }
1305         } else {
1306             $filerecord->timecreated = $now;
1307         }
1309         if (isset($filerecord->timemodified)) {
1310             if (!is_number($filerecord->timemodified)) {
1311                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1312             }
1313             if ($filerecord->timemodified < 0) {
1314                 //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)
1315                 $filerecord->timemodified = 0;
1316             }
1317         } else {
1318             $filerecord->timemodified = $now;
1319         }
1321         $newrecord = new stdClass();
1323         $newrecord->contextid = $filerecord->contextid;
1324         $newrecord->component = $filerecord->component;
1325         $newrecord->filearea  = $filerecord->filearea;
1326         $newrecord->itemid    = $filerecord->itemid;
1327         $newrecord->filepath  = $filerecord->filepath;
1328         $newrecord->filename  = $filerecord->filename;
1330         $newrecord->timecreated  = $filerecord->timecreated;
1331         $newrecord->timemodified = $filerecord->timemodified;
1332         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1333         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1334         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1335         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1336         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1337         $newrecord->sortorder    = $filerecord->sortorder;
1339         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1340         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1341         // get mimetype by magic bytes
1342         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1344         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1346         try {
1347             $newrecord->id = $DB->insert_record('files', $newrecord);
1348         } catch (dml_exception $e) {
1349             if ($newfile) {
1350                 $this->deleted_file_cleanup($newrecord->contenthash);
1351             }
1352             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1353                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1354         }
1356         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1358         return $this->get_file_instance($newrecord);
1359     }
1361     /**
1362      * Create a new alias/shortcut file from file reference information
1363      *
1364      * @param stdClass|array $filerecord object or array describing the new file
1365      * @param int $repositoryid the id of the repository that provides the original file
1366      * @param string $reference the information required by the repository to locate the original file
1367      * @param array $options options for creating the new file
1368      * @return stored_file
1369      */
1370     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1371         global $DB;
1373         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1374         $filerecord = (object)$filerecord; // We support arrays too.
1376         // validate all parameters, we do not want any rubbish stored in database, right?
1377         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1378             throw new file_exception('storedfileproblem', 'Invalid contextid');
1379         }
1381         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1382         if (empty($filerecord->component)) {
1383             throw new file_exception('storedfileproblem', 'Invalid component');
1384         }
1386         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1387         if (empty($filerecord->filearea)) {
1388             throw new file_exception('storedfileproblem', 'Invalid filearea');
1389         }
1391         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1392             throw new file_exception('storedfileproblem', 'Invalid itemid');
1393         }
1395         if (!empty($filerecord->sortorder)) {
1396             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1397                 $filerecord->sortorder = 0;
1398             }
1399         } else {
1400             $filerecord->sortorder = 0;
1401         }
1403         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1404         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1405         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1406         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1407         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1408         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1409         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1410         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1411             // Path must start and end with '/'.
1412             throw new file_exception('storedfileproblem', 'Invalid file path');
1413         }
1415         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1416         if ($filerecord->filename === '') {
1417             // Path must start and end with '/'.
1418             throw new file_exception('storedfileproblem', 'Invalid file name');
1419         }
1421         $now = time();
1422         if (isset($filerecord->timecreated)) {
1423             if (!is_number($filerecord->timecreated)) {
1424                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1425             }
1426             if ($filerecord->timecreated < 0) {
1427                 // 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)
1428                 $filerecord->timecreated = 0;
1429             }
1430         } else {
1431             $filerecord->timecreated = $now;
1432         }
1434         if (isset($filerecord->timemodified)) {
1435             if (!is_number($filerecord->timemodified)) {
1436                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1437             }
1438             if ($filerecord->timemodified < 0) {
1439                 // 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)
1440                 $filerecord->timemodified = 0;
1441             }
1442         } else {
1443             $filerecord->timemodified = $now;
1444         }
1446         $transaction = $DB->start_delegated_transaction();
1448         try {
1449             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1450         } catch (Exception $e) {
1451             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1452         }
1454         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1455             // there was specified the contenthash for a file already stored in moodle filepool
1456             if (empty($filerecord->filesize)) {
1457                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1458                 $filerecord->filesize = filesize($filepathname);
1459             } else {
1460                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1461             }
1462         } else {
1463             // atempt to get the result of last synchronisation for this reference
1464             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1465                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1466             if ($lastcontent) {
1467                 $filerecord->contenthash = $lastcontent->contenthash;
1468                 $filerecord->filesize = $lastcontent->filesize;
1469             } else {
1470                 // External file doesn't have content in moodle.
1471                 // So we create an empty file for it.
1472                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1473             }
1474         }
1476         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1478         try {
1479             $filerecord->id = $DB->insert_record('files', $filerecord);
1480         } catch (dml_exception $e) {
1481             if (!empty($newfile)) {
1482                 $this->deleted_file_cleanup($filerecord->contenthash);
1483             }
1484             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1485                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1486         }
1488         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1490         $transaction->allow_commit();
1492         // this will retrieve all reference information from DB as well
1493         return $this->get_file_by_id($filerecord->id);
1494     }
1496     /**
1497      * Creates new image file from existing.
1498      *
1499      * @param stdClass|array $filerecord object or array describing new file
1500      * @param int|stored_file $fid file id or stored file object
1501      * @param int $newwidth in pixels
1502      * @param int $newheight in pixels
1503      * @param bool $keepaspectratio whether or not keep aspect ratio
1504      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1505      * @return stored_file
1506      */
1507     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1508         if (!function_exists('imagecreatefromstring')) {
1509             //Most likely the GD php extension isn't installed
1510             //image conversion cannot succeed
1511             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1512         }
1514         if ($fid instanceof stored_file) {
1515             $fid = $fid->get_id();
1516         }
1518         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1520         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1521             throw new file_exception('storedfileproblem', 'File does not exist');
1522         }
1524         if (!$imageinfo = $file->get_imageinfo()) {
1525             throw new file_exception('storedfileproblem', 'File is not an image');
1526         }
1528         if (!isset($filerecord['filename'])) {
1529             $filerecord['filename'] = $file->get_filename();
1530         }
1532         if (!isset($filerecord['mimetype'])) {
1533             $filerecord['mimetype'] = $imageinfo['mimetype'];
1534         }
1536         $width    = $imageinfo['width'];
1537         $height   = $imageinfo['height'];
1538         $mimetype = $imageinfo['mimetype'];
1540         if ($keepaspectratio) {
1541             if (0 >= $newwidth and 0 >= $newheight) {
1542                 // no sizes specified
1543                 $newwidth  = $width;
1544                 $newheight = $height;
1546             } else if (0 < $newwidth and 0 < $newheight) {
1547                 $xheight = ($newwidth*($height/$width));
1548                 if ($xheight < $newheight) {
1549                     $newheight = (int)$xheight;
1550                 } else {
1551                     $newwidth = (int)($newheight*($width/$height));
1552                 }
1554             } else if (0 < $newwidth) {
1555                 $newheight = (int)($newwidth*($height/$width));
1557             } else { //0 < $newheight
1558                 $newwidth = (int)($newheight*($width/$height));
1559             }
1561         } else {
1562             if (0 >= $newwidth) {
1563                 $newwidth = $width;
1564             }
1565             if (0 >= $newheight) {
1566                 $newheight = $height;
1567             }
1568         }
1570         $img = imagecreatefromstring($file->get_content());
1571         if ($height != $newheight or $width != $newwidth) {
1572             $newimg = imagecreatetruecolor($newwidth, $newheight);
1573             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1574                 // weird
1575                 throw new file_exception('storedfileproblem', 'Can not resize image');
1576             }
1577             imagedestroy($img);
1578             $img = $newimg;
1579         }
1581         ob_start();
1582         switch ($filerecord['mimetype']) {
1583             case 'image/gif':
1584                 imagegif($img);
1585                 break;
1587             case 'image/jpeg':
1588                 if (is_null($quality)) {
1589                     imagejpeg($img);
1590                 } else {
1591                     imagejpeg($img, NULL, $quality);
1592                 }
1593                 break;
1595             case 'image/png':
1596                 $quality = (int)$quality;
1597                 imagepng($img, NULL, $quality, NULL);
1598                 break;
1600             default:
1601                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1602         }
1604         $content = ob_get_contents();
1605         ob_end_clean();
1606         imagedestroy($img);
1608         if (!$content) {
1609             throw new file_exception('storedfileproblem', 'Can not convert image');
1610         }
1612         return $this->create_file_from_string($filerecord, $content);
1613     }
1615     /**
1616      * Add file content to sha1 pool.
1617      *
1618      * @param string $pathname path to file
1619      * @param string $contenthash sha1 hash of content if known (performance only)
1620      * @return array (contenthash, filesize, newfile)
1621      */
1622     public function add_file_to_pool($pathname, $contenthash = NULL) {
1623         global $CFG;
1625         if (!is_readable($pathname)) {
1626             throw new file_exception('storedfilecannotread', '', $pathname);
1627         }
1629         $filesize = filesize($pathname);
1630         if ($filesize === false) {
1631             throw new file_exception('storedfilecannotread', '', $pathname);
1632         }
1634         if (is_null($contenthash)) {
1635             $contenthash = sha1_file($pathname);
1636         } else if ($CFG->debugdeveloper) {
1637             $filehash = sha1_file($pathname);
1638             if ($filehash === false) {
1639                 throw new file_exception('storedfilecannotread', '', $pathname);
1640             }
1641             if ($filehash !== $contenthash) {
1642                 // Hopefully this never happens, if yes we need to fix calling code.
1643                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1644                 $contenthash = $filehash;
1645             }
1646         }
1647         if ($contenthash === false) {
1648             throw new file_exception('storedfilecannotread', '', $pathname);
1649         }
1651         if ($filesize > 0 and $contenthash === sha1('')) {
1652             // Did the file change or is sha1_file() borked for this file?
1653             clearstatcache();
1654             $contenthash = sha1_file($pathname);
1655             $filesize = filesize($pathname);
1657             if ($contenthash === false or $filesize === false) {
1658                 throw new file_exception('storedfilecannotread', '', $pathname);
1659             }
1660             if ($filesize > 0 and $contenthash === sha1('')) {
1661                 // This is very weird...
1662                 throw new file_exception('storedfilecannotread', '', $pathname);
1663             }
1664         }
1666         $hashpath = $this->path_from_hash($contenthash);
1667         $hashfile = "$hashpath/$contenthash";
1669         $newfile = true;
1671         if (file_exists($hashfile)) {
1672             if (filesize($hashfile) === $filesize) {
1673                 return array($contenthash, $filesize, false);
1674             }
1675             if (sha1_file($hashfile) === $contenthash) {
1676                 // Jackpot! We have a sha1 collision.
1677                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1678                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1679                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1680                 throw new file_pool_content_exception($contenthash);
1681             }
1682             debugging("Replacing invalid content file $contenthash");
1683             unlink($hashfile);
1684             $newfile = false;
1685         }
1687         if (!is_dir($hashpath)) {
1688             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1689                 // Permission trouble.
1690                 throw new file_exception('storedfilecannotcreatefiledirs');
1691             }
1692         }
1694         // Let's try to prevent some race conditions.
1696         $prev = ignore_user_abort(true);
1697         @unlink($hashfile.'.tmp');
1698         if (!copy($pathname, $hashfile.'.tmp')) {
1699             // Borked permissions or out of disk space.
1700             ignore_user_abort($prev);
1701             throw new file_exception('storedfilecannotcreatefile');
1702         }
1703         if (filesize($hashfile.'.tmp') !== $filesize) {
1704             // This should not happen.
1705             unlink($hashfile.'.tmp');
1706             ignore_user_abort($prev);
1707             throw new file_exception('storedfilecannotcreatefile');
1708         }
1709         rename($hashfile.'.tmp', $hashfile);
1710         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1711         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1712         ignore_user_abort($prev);
1714         return array($contenthash, $filesize, $newfile);
1715     }
1717     /**
1718      * Add string content to sha1 pool.
1719      *
1720      * @param string $content file content - binary string
1721      * @return array (contenthash, filesize, newfile)
1722      */
1723     public function add_string_to_pool($content) {
1724         $contenthash = sha1($content);
1725         $filesize = strlen($content); // binary length
1727         $hashpath = $this->path_from_hash($contenthash);
1728         $hashfile = "$hashpath/$contenthash";
1730         $newfile = true;
1732         if (file_exists($hashfile)) {
1733             if (filesize($hashfile) === $filesize) {
1734                 return array($contenthash, $filesize, false);
1735             }
1736             if (sha1_file($hashfile) === $contenthash) {
1737                 // Jackpot! We have a sha1 collision.
1738                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1739                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
1740                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
1741                 throw new file_pool_content_exception($contenthash);
1742             }
1743             debugging("Replacing invalid content file $contenthash");
1744             unlink($hashfile);
1745             $newfile = false;
1746         }
1748         if (!is_dir($hashpath)) {
1749             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1750                 // Permission trouble.
1751                 throw new file_exception('storedfilecannotcreatefiledirs');
1752             }
1753         }
1755         // Hopefully this works around most potential race conditions.
1757         $prev = ignore_user_abort(true);
1758         $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
1759         if ($newsize === false) {
1760             // Borked permissions most likely.
1761             ignore_user_abort($prev);
1762             throw new file_exception('storedfilecannotcreatefile');
1763         }
1764         if (filesize($hashfile.'.tmp') !== $filesize) {
1765             // Out of disk space?
1766             unlink($hashfile.'.tmp');
1767             ignore_user_abort($prev);
1768             throw new file_exception('storedfilecannotcreatefile');
1769         }
1770         rename($hashfile.'.tmp', $hashfile);
1771         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1772         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1773         ignore_user_abort($prev);
1775         return array($contenthash, $filesize, $newfile);
1776     }
1778     /**
1779      * Serve file content using X-Sendfile header.
1780      * Please make sure that all headers are already sent
1781      * and the all access control checks passed.
1782      *
1783      * @param string $contenthash sah1 hash of the file content to be served
1784      * @return bool success
1785      */
1786     public function xsendfile($contenthash) {
1787         global $CFG;
1788         require_once("$CFG->libdir/xsendfilelib.php");
1790         $hashpath = $this->path_from_hash($contenthash);
1791         return xsendfile("$hashpath/$contenthash");
1792     }
1794     /**
1795      * Content exists
1796      *
1797      * @param string $contenthash
1798      * @return bool
1799      */
1800     public function content_exists($contenthash) {
1801         $dir = $this->path_from_hash($contenthash);
1802         $filepath = $dir . '/' . $contenthash;
1803         return file_exists($filepath);
1804     }
1806     /**
1807      * Return path to file with given hash.
1808      *
1809      * NOTE: must not be public, files in pool must not be modified
1810      *
1811      * @param string $contenthash content hash
1812      * @return string expected file location
1813      */
1814     protected function path_from_hash($contenthash) {
1815         $l1 = $contenthash[0].$contenthash[1];
1816         $l2 = $contenthash[2].$contenthash[3];
1817         return "$this->filedir/$l1/$l2";
1818     }
1820     /**
1821      * Return path to file with given hash.
1822      *
1823      * NOTE: must not be public, files in pool must not be modified
1824      *
1825      * @param string $contenthash content hash
1826      * @return string expected file location
1827      */
1828     protected function trash_path_from_hash($contenthash) {
1829         $l1 = $contenthash[0].$contenthash[1];
1830         $l2 = $contenthash[2].$contenthash[3];
1831         return "$this->trashdir/$l1/$l2";
1832     }
1834     /**
1835      * Tries to recover missing content of file from trash.
1836      *
1837      * @param stored_file $file stored_file instance
1838      * @return bool success
1839      */
1840     public function try_content_recovery($file) {
1841         $contenthash = $file->get_contenthash();
1842         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1843         if (!is_readable($trashfile)) {
1844             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1845                 return false;
1846             }
1847             // nice, at least alternative trash file in trash root exists
1848             $trashfile = $this->trashdir.'/'.$contenthash;
1849         }
1850         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1851             //weird, better fail early
1852             return false;
1853         }
1854         $contentdir  = $this->path_from_hash($contenthash);
1855         $contentfile = $contentdir.'/'.$contenthash;
1856         if (file_exists($contentfile)) {
1857             //strange, no need to recover anything
1858             return true;
1859         }
1860         if (!is_dir($contentdir)) {
1861             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1862                 return false;
1863             }
1864         }
1865         return rename($trashfile, $contentfile);
1866     }
1868     /**
1869      * Marks pool file as candidate for deleting.
1870      *
1871      * DO NOT call directly - reserved for core!!
1872      *
1873      * @param string $contenthash
1874      */
1875     public function deleted_file_cleanup($contenthash) {
1876         global $DB;
1878         if ($contenthash === sha1('')) {
1879             // No need to delete empty content file with sha1('') content hash.
1880             return;
1881         }
1883         //Note: this section is critical - in theory file could be reused at the same
1884         //      time, if this happens we can still recover the file from trash
1885         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1886             // file content is still used
1887             return;
1888         }
1889         //move content file to trash
1890         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1891         if (!file_exists($contentfile)) {
1892             //weird, but no problem
1893             return;
1894         }
1895         $trashpath = $this->trash_path_from_hash($contenthash);
1896         $trashfile = $trashpath.'/'.$contenthash;
1897         if (file_exists($trashfile)) {
1898             // we already have this content in trash, no need to move it there
1899             unlink($contentfile);
1900             return;
1901         }
1902         if (!is_dir($trashpath)) {
1903             mkdir($trashpath, $this->dirpermissions, true);
1904         }
1905         rename($contentfile, $trashfile);
1906         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1907     }
1909     /**
1910      * When user referring to a moodle file, we build the reference field
1911      *
1912      * @param array $params
1913      * @return string
1914      */
1915     public static function pack_reference($params) {
1916         $params = (array)$params;
1917         $reference = array();
1918         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1919         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1920         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1921         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1922         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1923         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1924         return base64_encode(serialize($reference));
1925     }
1927     /**
1928      * Unpack reference field
1929      *
1930      * @param string $str
1931      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1932      * @throws file_reference_exception if the $str does not have the expected format
1933      * @return array
1934      */
1935     public static function unpack_reference($str, $cleanparams = false) {
1936         $decoded = base64_decode($str, true);
1937         if ($decoded === false) {
1938             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1939         }
1940         $params = @unserialize($decoded); // hide E_NOTICE
1941         if ($params === false) {
1942             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1943         }
1944         if (is_array($params) && $cleanparams) {
1945             $params = array(
1946                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1947                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1948                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1949                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1950                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1951                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1952             );
1953         }
1954         return $params;
1955     }
1957     /**
1958      * Returns all aliases that refer to some stored_file via the given reference
1959      *
1960      * All repositories that provide access to a stored_file are expected to use
1961      * {@link self::pack_reference()}. This method can't be used if the given reference
1962      * does not use this format or if you are looking for references to an external file
1963      * (for example it can't be used to search for all aliases that refer to a given
1964      * Dropbox or Box.net file).
1965      *
1966      * Aliases in user draft areas are excluded from the returned list.
1967      *
1968      * @param string $reference identification of the referenced file
1969      * @return array of stored_file indexed by its pathnamehash
1970      */
1971     public function search_references($reference) {
1972         global $DB;
1974         if (is_null($reference)) {
1975             throw new coding_exception('NULL is not a valid reference to an external file');
1976         }
1978         // Give {@link self::unpack_reference()} a chance to throw exception if the
1979         // reference is not in a valid format.
1980         self::unpack_reference($reference);
1982         $referencehash = sha1($reference);
1984         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1985                   FROM {files} f
1986                   JOIN {files_reference} r ON f.referencefileid = r.id
1987                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1988                  WHERE r.referencehash = ?
1989                        AND (f.component <> ? OR f.filearea <> ?)";
1991         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1992         $files = array();
1993         foreach ($rs as $filerecord) {
1994             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1995         }
1997         return $files;
1998     }
2000     /**
2001      * Returns the number of aliases that refer to some stored_file via the given reference
2002      *
2003      * All repositories that provide access to a stored_file are expected to use
2004      * {@link self::pack_reference()}. This method can't be used if the given reference
2005      * does not use this format or if you are looking for references to an external file
2006      * (for example it can't be used to count aliases that refer to a given Dropbox or
2007      * Box.net file).
2008      *
2009      * Aliases in user draft areas are not counted.
2010      *
2011      * @param string $reference identification of the referenced file
2012      * @return int
2013      */
2014     public function search_references_count($reference) {
2015         global $DB;
2017         if (is_null($reference)) {
2018             throw new coding_exception('NULL is not a valid reference to an external file');
2019         }
2021         // Give {@link self::unpack_reference()} a chance to throw exception if the
2022         // reference is not in a valid format.
2023         self::unpack_reference($reference);
2025         $referencehash = sha1($reference);
2027         $sql = "SELECT COUNT(f.id)
2028                   FROM {files} f
2029                   JOIN {files_reference} r ON f.referencefileid = r.id
2030                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2031                  WHERE r.referencehash = ?
2032                        AND (f.component <> ? OR f.filearea <> ?)";
2034         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2035     }
2037     /**
2038      * Returns all aliases that link to the given stored_file
2039      *
2040      * Aliases in user draft areas are excluded from the returned list.
2041      *
2042      * @param stored_file $storedfile
2043      * @return array of stored_file
2044      */
2045     public function get_references_by_storedfile(stored_file $storedfile) {
2046         global $DB;
2048         $params = array();
2049         $params['contextid'] = $storedfile->get_contextid();
2050         $params['component'] = $storedfile->get_component();
2051         $params['filearea']  = $storedfile->get_filearea();
2052         $params['itemid']    = $storedfile->get_itemid();
2053         $params['filename']  = $storedfile->get_filename();
2054         $params['filepath']  = $storedfile->get_filepath();
2056         return $this->search_references(self::pack_reference($params));
2057     }
2059     /**
2060      * Returns the number of aliases that link to the given stored_file
2061      *
2062      * Aliases in user draft areas are not counted.
2063      *
2064      * @param stored_file $storedfile
2065      * @return int
2066      */
2067     public function get_references_count_by_storedfile(stored_file $storedfile) {
2068         global $DB;
2070         $params = array();
2071         $params['contextid'] = $storedfile->get_contextid();
2072         $params['component'] = $storedfile->get_component();
2073         $params['filearea']  = $storedfile->get_filearea();
2074         $params['itemid']    = $storedfile->get_itemid();
2075         $params['filename']  = $storedfile->get_filename();
2076         $params['filepath']  = $storedfile->get_filepath();
2078         return $this->search_references_count(self::pack_reference($params));
2079     }
2081     /**
2082      * Updates all files that are referencing this file with the new contenthash
2083      * and filesize
2084      *
2085      * @param stored_file $storedfile
2086      */
2087     public function update_references_to_storedfile(stored_file $storedfile) {
2088         global $CFG, $DB;
2089         $params = array();
2090         $params['contextid'] = $storedfile->get_contextid();
2091         $params['component'] = $storedfile->get_component();
2092         $params['filearea']  = $storedfile->get_filearea();
2093         $params['itemid']    = $storedfile->get_itemid();
2094         $params['filename']  = $storedfile->get_filename();
2095         $params['filepath']  = $storedfile->get_filepath();
2096         $reference = self::pack_reference($params);
2097         $referencehash = sha1($reference);
2099         $sql = "SELECT repositoryid, id FROM {files_reference}
2100                  WHERE referencehash = ?";
2101         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2103         $now = time();
2104         foreach ($rs as $record) {
2105             require_once($CFG->dirroot.'/repository/lib.php');
2106             $repo = repository::get_instance($record->repositoryid);
2107             $lifetime = $repo->get_reference_file_lifetime($reference);
2108             $this->update_references($record->id, $now, $lifetime,
2109                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
2110         }
2111         $rs->close();
2112     }
2114     /**
2115      * Convert file alias to local file
2116      *
2117      * @throws moodle_exception if file could not be downloaded
2118      *
2119      * @param stored_file $storedfile a stored_file instances
2120      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2121      * @return stored_file stored_file
2122      */
2123     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2124         global $CFG;
2125         $storedfile->import_external_file_contents($maxbytes);
2126         $storedfile->delete_reference();
2127         return $storedfile;
2128     }
2130     /**
2131      * Return mimetype by given file pathname
2132      *
2133      * If file has a known extension, we return the mimetype based on extension.
2134      * Otherwise (when possible) we try to get the mimetype from file contents.
2135      *
2136      * @param string $pathname full path to the file
2137      * @param string $filename correct file name with extension, if omitted will be taken from $path
2138      * @return string
2139      */
2140     public static function mimetype($pathname, $filename = null) {
2141         if (empty($filename)) {
2142             $filename = $pathname;
2143         }
2144         $type = mimeinfo('type', $filename);
2145         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2146             $finfo = new finfo(FILEINFO_MIME_TYPE);
2147             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2148         }
2149         return $type;
2150     }
2152     /**
2153      * Cron cleanup job.
2154      */
2155     public function cron() {
2156         global $CFG, $DB;
2157         require_once($CFG->libdir.'/cronlib.php');
2159         // find out all stale draft areas (older than 4 days) and purge them
2160         // those are identified by time stamp of the /. root dir
2161         mtrace('Deleting old draft files... ', '');
2162         cron_trace_time_and_memory();
2163         $old = time() - 60*60*24*4;
2164         $sql = "SELECT *
2165                   FROM {files}
2166                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2167                        AND timecreated < :old";
2168         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2169         foreach ($rs as $dir) {
2170             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2171         }
2172         $rs->close();
2173         mtrace('done.');
2175         // remove orphaned preview files (that is files in the core preview filearea without
2176         // the existing original file)
2177         mtrace('Deleting orphaned preview files... ', '');
2178         cron_trace_time_and_memory();
2179         $sql = "SELECT p.*
2180                   FROM {files} p
2181              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2182                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2183                        AND o.id IS NULL";
2184         $syscontext = context_system::instance();
2185         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2186         foreach ($rs as $orphan) {
2187             $file = $this->get_file_instance($orphan);
2188             if (!$file->is_directory()) {
2189                 $file->delete();
2190             }
2191         }
2192         $rs->close();
2193         mtrace('done.');
2195         // remove trash pool files once a day
2196         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2197         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2198             require_once($CFG->libdir.'/filelib.php');
2199             // Delete files that are associated with a context that no longer exists.
2200             mtrace('Cleaning up files from deleted contexts... ', '');
2201             cron_trace_time_and_memory();
2202             $sql = "SELECT DISTINCT f.contextid
2203                     FROM {files} f
2204                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2205                     WHERE c.id IS NULL";
2206             $rs = $DB->get_recordset_sql($sql);
2207             if ($rs->valid()) {
2208                 $fs = get_file_storage();
2209                 foreach ($rs as $ctx) {
2210                     $fs->delete_area_files($ctx->contextid);
2211                 }
2212             }
2213             $rs->close();
2214             mtrace('done.');
2216             mtrace('Deleting trash files... ', '');
2217             cron_trace_time_and_memory();
2218             fulldelete($this->trashdir);
2219             set_config('fileslastcleanup', time());
2220             mtrace('done.');
2221         }
2222     }
2224     /**
2225      * Get the sql formated fields for a file instance to be created from a
2226      * {files} and {files_refernece} join.
2227      *
2228      * @param string $filesprefix the table prefix for the {files} table
2229      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2230      * @return string the sql to go after a SELECT
2231      */
2232     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2233         // Note, these fieldnames MUST NOT overlap between the two tables,
2234         // else problems like MDL-33172 occur.
2235         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2236             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2237             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2239         $referencefields = array('repositoryid' => 'repositoryid',
2240             'reference' => 'reference',
2241             'lastsync' => 'referencelastsync',
2242             'lifetime' => 'referencelifetime');
2244         // id is specifically named to prevent overlaping between the two tables.
2245         $fields = array();
2246         $fields[] = $filesprefix.'.id AS id';
2247         foreach ($filefields as $field) {
2248             $fields[] = "{$filesprefix}.{$field}";
2249         }
2251         foreach ($referencefields as $field => $alias) {
2252             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2253         }
2255         return implode(', ', $fields);
2256     }
2258     /**
2259      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2260      *
2261      * If the record already exists, its id is returned. If there is no such record yet,
2262      * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
2263      *
2264      * @param int $repositoryid
2265      * @param string $reference
2266      * @return int
2267      */
2268     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2269         global $DB;
2271         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2273         if ($id !== false) {
2274             // bah, that was easy
2275             return $id;
2276         }
2278         // no such record yet, create one
2279         try {
2280             $id = $DB->insert_record('files_reference', array(
2281                 'repositoryid'  => $repositoryid,
2282                 'reference'     => $reference,
2283                 'referencehash' => sha1($reference),
2284                 'lastsync'      => $lastsync,
2285                 'lifetime'      => $lifetime));
2286         } catch (dml_exception $e) {
2287             // if inserting the new record failed, chances are that the race condition has just
2288             // occured and the unique index did not allow to create the second record with the same
2289             // repositoryid + reference combo
2290             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2291         }
2293         return $id;
2294     }
2296     /**
2297      * Returns the id of the record in {files_reference} that matches the passed parameters
2298      *
2299      * Depending on the required strictness, false can be returned. The behaviour is consistent
2300      * with standard DML methods.
2301      *
2302      * @param int $repositoryid
2303      * @param string $reference
2304      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2305      * @return int|bool
2306      */
2307     private function get_referencefileid($repositoryid, $reference, $strictness) {
2308         global $DB;
2310         return $DB->get_field('files_reference', 'id',
2311             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2312     }
2314     /**
2315      * Updates a reference to the external resource and all files that use it
2316      *
2317      * This function is called after synchronisation of an external file and updates the
2318      * contenthash, filesize and status of all files that reference this external file
2319      * as well as time last synchronised and sync lifetime (how long we don't need to call
2320      * synchronisation for this reference).
2321      *
2322      * @param int $referencefileid
2323      * @param int $lastsync
2324      * @param int $lifetime
2325      * @param string $contenthash
2326      * @param int $filesize
2327      * @param int $status 0 if ok or 666 if source is missing
2328      */
2329     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2330         global $DB;
2331         $referencefileid = clean_param($referencefileid, PARAM_INT);
2332         $lastsync = clean_param($lastsync, PARAM_INT);
2333         $lifetime = clean_param($lifetime, PARAM_INT);
2334         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2335         $filesize = clean_param($filesize, PARAM_INT);
2336         $status = clean_param($status, PARAM_INT);
2337         $params = array('contenthash' => $contenthash,
2338                     'filesize' => $filesize,
2339                     'status' => $status,
2340                     'referencefileid' => $referencefileid,
2341                     'lastsync' => $lastsync,
2342                     'lifetime' => $lifetime);
2343         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2344             status = :status
2345             WHERE referencefileid = :referencefileid', $params);
2346         $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2347         $DB->update_record('files_reference', (object)$data);
2348     }