f3aca13f889bf36146cba9cb1249535a39947414
[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 Moodle 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 Moodle 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         $context = context_system::instance();
359         $record = array(
360             'contextid' => $context->id,
361             'component' => 'core',
362             'filearea'  => 'preview',
363             'itemid'    => 0,
364             'filepath'  => '/' . trim($mode, '/') . '/',
365             'filename'  => $file->get_contenthash(),
366         );
368         $imageinfo = getimagesizefromstring($data);
369         if ($imageinfo) {
370             $record['mimetype'] = $imageinfo['mime'];
371         }
373         return $this->create_file_from_string($record, $data);
374     }
376     /**
377      * Generates a preview for the stored image file
378      *
379      * @param stored_file $file the image we want to preview
380      * @param string $mode preview mode, eg. 'thumb'
381      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
382      */
383     protected function create_imagefile_preview(stored_file $file, $mode) {
384         global $CFG;
385         require_once($CFG->libdir.'/gdlib.php');
387         $tmproot = make_temp_directory('thumbnails');
388         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
389         $file->copy_content_to($tmpfilepath);
391         if ($mode === 'tinyicon') {
392             $data = generate_image_thumbnail($tmpfilepath, 24, 24);
394         } else if ($mode === 'thumb') {
395             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
397         } else if ($mode === 'bigthumb') {
398             $data = generate_image_thumbnail($tmpfilepath, 250, 250);
400         } else {
401             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
402         }
404         unlink($tmpfilepath);
406         return $data;
407     }
409     /**
410      * Fetch file using local file id.
411      *
412      * Please do not rely on file ids, it is usually easier to use
413      * pathname hashes instead.
414      *
415      * @param int $fileid file ID
416      * @return stored_file|bool stored_file instance if exists, false if not
417      */
418     public function get_file_by_id($fileid) {
419         global $DB;
421         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
422                   FROM {files} f
423              LEFT JOIN {files_reference} r
424                        ON f.referencefileid = r.id
425                  WHERE f.id = ?";
426         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
427             return $this->get_file_instance($filerecord);
428         } else {
429             return false;
430         }
431     }
433     /**
434      * Fetch file using local file full pathname hash
435      *
436      * @param string $pathnamehash path name hash
437      * @return stored_file|bool stored_file instance if exists, false if not
438      */
439     public function get_file_by_hash($pathnamehash) {
440         global $DB;
442         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
443                   FROM {files} f
444              LEFT JOIN {files_reference} r
445                        ON f.referencefileid = r.id
446                  WHERE f.pathnamehash = ?";
447         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
448             return $this->get_file_instance($filerecord);
449         } else {
450             return false;
451         }
452     }
454     /**
455      * Fetch locally stored file.
456      *
457      * @param int $contextid context ID
458      * @param string $component component
459      * @param string $filearea file area
460      * @param int $itemid item ID
461      * @param string $filepath file path
462      * @param string $filename file name
463      * @return stored_file|bool stored_file instance if exists, false if not
464      */
465     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
466         $filepath = clean_param($filepath, PARAM_PATH);
467         $filename = clean_param($filename, PARAM_FILE);
469         if ($filename === '') {
470             $filename = '.';
471         }
473         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
474         return $this->get_file_by_hash($pathnamehash);
475     }
477     /**
478      * Are there any files (or directories)
479      *
480      * @param int $contextid context ID
481      * @param string $component component
482      * @param string $filearea file area
483      * @param bool|int $itemid item id or false if all items
484      * @param bool $ignoredirs whether or not ignore directories
485      * @return bool empty
486      */
487     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
488         global $DB;
490         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
491         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
493         if ($itemid !== false) {
494             $params['itemid'] = $itemid;
495             $where .= " AND itemid = :itemid";
496         }
498         if ($ignoredirs) {
499             $sql = "SELECT 'x'
500                       FROM {files}
501                      WHERE $where AND filename <> '.'";
502         } else {
503             $sql = "SELECT 'x'
504                       FROM {files}
505                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
506         }
508         return !$DB->record_exists_sql($sql, $params);
509     }
511     /**
512      * Returns all files belonging to given repository
513      *
514      * @param int $repositoryid
515      * @param string $sort A fragment of SQL to use for sorting
516      */
517     public function get_external_files($repositoryid, $sort = '') {
518         global $DB;
519         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
520                   FROM {files} f
521              LEFT JOIN {files_reference} r
522                        ON f.referencefileid = r.id
523                  WHERE r.repositoryid = ?";
524         if (!empty($sort)) {
525             $sql .= " ORDER BY {$sort}";
526         }
528         $result = array();
529         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
530         foreach ($filerecords as $filerecord) {
531             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
532         }
533         return $result;
534     }
536     /**
537      * Returns all area files (optionally limited by itemid)
538      *
539      * @param int $contextid context ID
540      * @param string $component component
541      * @param string $filearea file area
542      * @param int $itemid item ID or all files if not specified
543      * @param string $sort A fragment of SQL to use for sorting
544      * @param bool $includedirs whether or not include directories
545      * @return stored_file[] array of stored_files indexed by pathanmehash
546      */
547     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
548         global $DB;
550         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
551         if ($itemid !== false) {
552             $itemidsql = ' AND f.itemid = :itemid ';
553             $conditions['itemid'] = $itemid;
554         } else {
555             $itemidsql = '';
556         }
558         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
559                   FROM {files} f
560              LEFT JOIN {files_reference} r
561                        ON f.referencefileid = r.id
562                  WHERE f.contextid = :contextid
563                        AND f.component = :component
564                        AND f.filearea = :filearea
565                        $itemidsql";
566         if (!empty($sort)) {
567             $sql .= " ORDER BY {$sort}";
568         }
570         $result = array();
571         $filerecords = $DB->get_records_sql($sql, $conditions);
572         foreach ($filerecords as $filerecord) {
573             if (!$includedirs and $filerecord->filename === '.') {
574                 continue;
575             }
576             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
577         }
578         return $result;
579     }
581     /**
582      * Returns array based tree structure of area files
583      *
584      * @param int $contextid context ID
585      * @param string $component component
586      * @param string $filearea file area
587      * @param int $itemid item ID
588      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
589      */
590     public function get_area_tree($contextid, $component, $filearea, $itemid) {
591         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
592         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
593         // first create directory structure
594         foreach ($files as $hash=>$dir) {
595             if (!$dir->is_directory()) {
596                 continue;
597             }
598             unset($files[$hash]);
599             if ($dir->get_filepath() === '/') {
600                 $result['dirfile'] = $dir;
601                 continue;
602             }
603             $parts = explode('/', trim($dir->get_filepath(),'/'));
604             $pointer =& $result;
605             foreach ($parts as $part) {
606                 if ($part === '') {
607                     continue;
608                 }
609                 if (!isset($pointer['subdirs'][$part])) {
610                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
611                 }
612                 $pointer =& $pointer['subdirs'][$part];
613             }
614             $pointer['dirfile'] = $dir;
615             unset($pointer);
616         }
617         foreach ($files as $hash=>$file) {
618             $parts = explode('/', trim($file->get_filepath(),'/'));
619             $pointer =& $result;
620             foreach ($parts as $part) {
621                 if ($part === '') {
622                     continue;
623                 }
624                 $pointer =& $pointer['subdirs'][$part];
625             }
626             $pointer['files'][$file->get_filename()] = $file;
627             unset($pointer);
628         }
629         $result = $this->sort_area_tree($result);
630         return $result;
631     }
633     /**
634      * Sorts the result of {@link file_storage::get_area_tree()}.
635      *
636      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
637      * @return array of sorted results
638      */
639     protected function sort_area_tree($tree) {
640         foreach ($tree as $key => &$value) {
641             if ($key == 'subdirs') {
642                 core_collator::ksort($value, core_collator::SORT_NATURAL);
643                 foreach ($value as $subdirname => &$subtree) {
644                     $subtree = $this->sort_area_tree($subtree);
645                 }
646             } else if ($key == 'files') {
647                 core_collator::ksort($value, core_collator::SORT_NATURAL);
648             }
649         }
650         return $tree;
651     }
653     /**
654      * Returns all files and optionally directories
655      *
656      * @param int $contextid context ID
657      * @param string $component component
658      * @param string $filearea file area
659      * @param int $itemid item ID
660      * @param int $filepath directory path
661      * @param bool $recursive include all subdirectories
662      * @param bool $includedirs include files and directories
663      * @param string $sort A fragment of SQL to use for sorting
664      * @return array of stored_files indexed by pathanmehash
665      */
666     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
667         global $DB;
669         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
670             return array();
671         }
673         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
675         if ($recursive) {
677             $dirs = $includedirs ? "" : "AND filename <> '.'";
678             $length = core_text::strlen($filepath);
680             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
681                       FROM {files} f
682                  LEFT JOIN {files_reference} r
683                            ON f.referencefileid = r.id
684                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
685                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
686                            AND f.id <> :dirid
687                            $dirs
688                            $orderby";
689             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
691             $files = array();
692             $dirs  = array();
693             $filerecords = $DB->get_records_sql($sql, $params);
694             foreach ($filerecords as $filerecord) {
695                 if ($filerecord->filename == '.') {
696                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
697                 } else {
698                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
699                 }
700             }
701             $result = array_merge($dirs, $files);
703         } else {
704             $result = array();
705             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
707             $length = core_text::strlen($filepath);
709             if ($includedirs) {
710                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
711                           FROM {files} f
712                      LEFT JOIN {files_reference} r
713                                ON f.referencefileid = r.id
714                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
715                                AND f.itemid = :itemid AND f.filename = '.'
716                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
717                                AND f.id <> :dirid
718                                $orderby";
719                 $reqlevel = substr_count($filepath, '/') + 1;
720                 $filerecords = $DB->get_records_sql($sql, $params);
721                 foreach ($filerecords as $filerecord) {
722                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
723                         continue;
724                     }
725                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
726                 }
727             }
729             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
730                       FROM {files} f
731                  LEFT JOIN {files_reference} r
732                            ON f.referencefileid = r.id
733                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
734                            AND f.filepath = :filepath AND f.filename <> '.'
735                            $orderby";
737             $filerecords = $DB->get_records_sql($sql, $params);
738             foreach ($filerecords as $filerecord) {
739                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
740             }
741         }
743         return $result;
744     }
746     /**
747      * Delete all area files (optionally limited by itemid).
748      *
749      * @param int $contextid context ID
750      * @param string $component component
751      * @param string $filearea file area or all areas in context if not specified
752      * @param int $itemid item ID or all files if not specified
753      * @return bool success
754      */
755     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
756         global $DB;
758         $conditions = array('contextid'=>$contextid);
759         if ($component !== false) {
760             $conditions['component'] = $component;
761         }
762         if ($filearea !== false) {
763             $conditions['filearea'] = $filearea;
764         }
765         if ($itemid !== false) {
766             $conditions['itemid'] = $itemid;
767         }
769         $filerecords = $DB->get_records('files', $conditions);
770         foreach ($filerecords as $filerecord) {
771             $this->get_file_instance($filerecord)->delete();
772         }
774         return true; // BC only
775     }
777     /**
778      * Delete all the files from certain areas where itemid is limited by an
779      * arbitrary bit of SQL.
780      *
781      * @param int $contextid the id of the context the files belong to. Must be given.
782      * @param string $component the owning component. Must be given.
783      * @param string $filearea the file area name. Must be given.
784      * @param string $itemidstest an SQL fragment that the itemid must match. Used
785      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
786      *      and may not used named parameters called contextid, component or filearea.
787      * @param array $params any query params used by $itemidstest.
788      */
789     public function delete_area_files_select($contextid, $component,
790             $filearea, $itemidstest, array $params = null) {
791         global $DB;
793         $where = "contextid = :contextid
794                 AND component = :component
795                 AND filearea = :filearea
796                 AND itemid $itemidstest";
797         $params['contextid'] = $contextid;
798         $params['component'] = $component;
799         $params['filearea'] = $filearea;
801         $filerecords = $DB->get_recordset_select('files', $where, $params);
802         foreach ($filerecords as $filerecord) {
803             $this->get_file_instance($filerecord)->delete();
804         }
805         $filerecords->close();
806     }
808     /**
809      * Delete all files associated with the given component.
810      *
811      * @param string $component the component owning the file
812      */
813     public function delete_component_files($component) {
814         global $DB;
816         $filerecords = $DB->get_recordset('files', array('component' => $component));
817         foreach ($filerecords as $filerecord) {
818             $this->get_file_instance($filerecord)->delete();
819         }
820         $filerecords->close();
821     }
823     /**
824      * Move all the files in a file area from one context to another.
825      *
826      * @param int $oldcontextid the context the files are being moved from.
827      * @param int $newcontextid the context the files are being moved to.
828      * @param string $component the plugin that these files belong to.
829      * @param string $filearea the name of the file area.
830      * @param int $itemid file item ID
831      * @return int the number of files moved, for information.
832      */
833     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
834         // Note, this code is based on some code that Petr wrote in
835         // forum_move_attachments in mod/forum/lib.php. I moved it here because
836         // I needed it in the question code too.
837         $count = 0;
839         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
840         foreach ($oldfiles as $oldfile) {
841             $filerecord = new stdClass();
842             $filerecord->contextid = $newcontextid;
843             $this->create_file_from_storedfile($filerecord, $oldfile);
844             $count += 1;
845         }
847         if ($count) {
848             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
849         }
851         return $count;
852     }
854     /**
855      * Recursively creates directory.
856      *
857      * @param int $contextid context ID
858      * @param string $component component
859      * @param string $filearea file area
860      * @param int $itemid item ID
861      * @param string $filepath file path
862      * @param int $userid the user ID
863      * @return bool success
864      */
865     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
866         global $DB;
868         // validate all parameters, we do not want any rubbish stored in database, right?
869         if (!is_number($contextid) or $contextid < 1) {
870             throw new file_exception('storedfileproblem', 'Invalid contextid');
871         }
873         $component = clean_param($component, PARAM_COMPONENT);
874         if (empty($component)) {
875             throw new file_exception('storedfileproblem', 'Invalid component');
876         }
878         $filearea = clean_param($filearea, PARAM_AREA);
879         if (empty($filearea)) {
880             throw new file_exception('storedfileproblem', 'Invalid filearea');
881         }
883         if (!is_number($itemid) or $itemid < 0) {
884             throw new file_exception('storedfileproblem', 'Invalid itemid');
885         }
887         $filepath = clean_param($filepath, PARAM_PATH);
888         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
889             // path must start and end with '/'
890             throw new file_exception('storedfileproblem', 'Invalid file path');
891         }
893         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
895         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
896             return $dir_info;
897         }
899         static $contenthash = null;
900         if (!$contenthash) {
901             $this->add_string_to_pool('');
902             $contenthash = sha1('');
903         }
905         $now = time();
907         $dir_record = new stdClass();
908         $dir_record->contextid = $contextid;
909         $dir_record->component = $component;
910         $dir_record->filearea  = $filearea;
911         $dir_record->itemid    = $itemid;
912         $dir_record->filepath  = $filepath;
913         $dir_record->filename  = '.';
914         $dir_record->contenthash  = $contenthash;
915         $dir_record->filesize  = 0;
917         $dir_record->timecreated  = $now;
918         $dir_record->timemodified = $now;
919         $dir_record->mimetype     = null;
920         $dir_record->userid       = $userid;
922         $dir_record->pathnamehash = $pathnamehash;
924         $DB->insert_record('files', $dir_record);
925         $dir_info = $this->get_file_by_hash($pathnamehash);
927         if ($filepath !== '/') {
928             //recurse to parent dirs
929             $filepath = trim($filepath, '/');
930             $filepath = explode('/', $filepath);
931             array_pop($filepath);
932             $filepath = implode('/', $filepath);
933             $filepath = ($filepath === '') ? '/' : "/$filepath/";
934             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
935         }
937         return $dir_info;
938     }
940     /**
941      * Add new local file based on existing local file.
942      *
943      * @param stdClass|array $filerecord object or array describing changes
944      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
945      * @return stored_file instance of newly created file
946      */
947     public function create_file_from_storedfile($filerecord, $fileorid) {
948         global $DB;
950         if ($fileorid instanceof stored_file) {
951             $fid = $fileorid->get_id();
952         } else {
953             $fid = $fileorid;
954         }
956         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
958         unset($filerecord['id']);
959         unset($filerecord['filesize']);
960         unset($filerecord['contenthash']);
961         unset($filerecord['pathnamehash']);
963         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
964                   FROM {files} f
965              LEFT JOIN {files_reference} r
966                        ON f.referencefileid = r.id
967                  WHERE f.id = ?";
969         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
970             throw new file_exception('storedfileproblem', 'File does not exist');
971         }
973         unset($newrecord->id);
975         foreach ($filerecord as $key => $value) {
976             // validate all parameters, we do not want any rubbish stored in database, right?
977             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
978                 throw new file_exception('storedfileproblem', 'Invalid contextid');
979             }
981             if ($key == 'component') {
982                 $value = clean_param($value, PARAM_COMPONENT);
983                 if (empty($value)) {
984                     throw new file_exception('storedfileproblem', 'Invalid component');
985                 }
986             }
988             if ($key == 'filearea') {
989                 $value = clean_param($value, PARAM_AREA);
990                 if (empty($value)) {
991                     throw new file_exception('storedfileproblem', 'Invalid filearea');
992                 }
993             }
995             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
996                 throw new file_exception('storedfileproblem', 'Invalid itemid');
997             }
1000             if ($key == 'filepath') {
1001                 $value = clean_param($value, PARAM_PATH);
1002                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1003                     // path must start and end with '/'
1004                     throw new file_exception('storedfileproblem', 'Invalid file path');
1005                 }
1006             }
1008             if ($key == 'filename') {
1009                 $value = clean_param($value, PARAM_FILE);
1010                 if ($value === '') {
1011                     // path must start and end with '/'
1012                     throw new file_exception('storedfileproblem', 'Invalid file name');
1013                 }
1014             }
1016             if ($key === 'timecreated' or $key === 'timemodified') {
1017                 if (!is_number($value)) {
1018                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1019                 }
1020                 if ($value < 0) {
1021                     //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)
1022                     $value = 0;
1023                 }
1024             }
1026             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1027                 $value = clean_param($value, PARAM_INT);
1028             }
1030             $newrecord->$key = $value;
1031         }
1033         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1035         if ($newrecord->filename === '.') {
1036             // special case - only this function supports directories ;-)
1037             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1038             // update the existing directory with the new data
1039             $newrecord->id = $directory->get_id();
1040             $DB->update_record('files', $newrecord);
1041             return $this->get_file_instance($newrecord);
1042         }
1044         // note: referencefileid is copied from the original file so that
1045         // creating a new file from an existing alias creates new alias implicitly.
1046         // here we just check the database consistency.
1047         if (!empty($newrecord->repositoryid)) {
1048             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1049                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1050             }
1051         }
1053         try {
1054             $newrecord->id = $DB->insert_record('files', $newrecord);
1055         } catch (dml_exception $e) {
1056             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1057                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1058         }
1061         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1063         return $this->get_file_instance($newrecord);
1064     }
1066     /**
1067      * Add new local file.
1068      *
1069      * @param stdClass|array $filerecord object or array describing file
1070      * @param string $url the URL to the file
1071      * @param array $options {@link download_file_content()} options
1072      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1073      * @return stored_file
1074      */
1075     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1077         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1078         $filerecord = (object)$filerecord; // We support arrays too.
1080         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1081         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1082         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1083         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1084         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1085         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1086         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1088         if (!isset($filerecord->filename)) {
1089             $parts = explode('/', $url);
1090             $filename = array_pop($parts);
1091             $filerecord->filename = clean_param($filename, PARAM_FILE);
1092         }
1093         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1094         $filerecord->source = clean_param($source, PARAM_URL);
1096         if ($usetempfile) {
1097             check_dir_exists($this->tempdir);
1098             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1099             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1100             if ($content === false) {
1101                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1102             }
1103             try {
1104                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1105                 @unlink($tmpfile);
1106                 return $newfile;
1107             } catch (Exception $e) {
1108                 @unlink($tmpfile);
1109                 throw $e;
1110             }
1112         } else {
1113             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1114             if ($content === false) {
1115                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1116             }
1117             return $this->create_file_from_string($filerecord, $content);
1118         }
1119     }
1121     /**
1122      * Add new local file.
1123      *
1124      * @param stdClass|array $filerecord object or array describing file
1125      * @param string $pathname path to file or content of file
1126      * @return stored_file
1127      */
1128     public function create_file_from_pathname($filerecord, $pathname) {
1129         global $DB;
1131         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1132         $filerecord = (object)$filerecord; // We support arrays too.
1134         // validate all parameters, we do not want any rubbish stored in database, right?
1135         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1136             throw new file_exception('storedfileproblem', 'Invalid contextid');
1137         }
1139         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1140         if (empty($filerecord->component)) {
1141             throw new file_exception('storedfileproblem', 'Invalid component');
1142         }
1144         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1145         if (empty($filerecord->filearea)) {
1146             throw new file_exception('storedfileproblem', 'Invalid filearea');
1147         }
1149         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1150             throw new file_exception('storedfileproblem', 'Invalid itemid');
1151         }
1153         if (!empty($filerecord->sortorder)) {
1154             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1155                 $filerecord->sortorder = 0;
1156             }
1157         } else {
1158             $filerecord->sortorder = 0;
1159         }
1161         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1162         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1163             // path must start and end with '/'
1164             throw new file_exception('storedfileproblem', 'Invalid file path');
1165         }
1167         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1168         if ($filerecord->filename === '') {
1169             // filename must not be empty
1170             throw new file_exception('storedfileproblem', 'Invalid file name');
1171         }
1173         $now = time();
1174         if (isset($filerecord->timecreated)) {
1175             if (!is_number($filerecord->timecreated)) {
1176                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1177             }
1178             if ($filerecord->timecreated < 0) {
1179                 //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)
1180                 $filerecord->timecreated = 0;
1181             }
1182         } else {
1183             $filerecord->timecreated = $now;
1184         }
1186         if (isset($filerecord->timemodified)) {
1187             if (!is_number($filerecord->timemodified)) {
1188                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1189             }
1190             if ($filerecord->timemodified < 0) {
1191                 //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)
1192                 $filerecord->timemodified = 0;
1193             }
1194         } else {
1195             $filerecord->timemodified = $now;
1196         }
1198         $newrecord = new stdClass();
1200         $newrecord->contextid = $filerecord->contextid;
1201         $newrecord->component = $filerecord->component;
1202         $newrecord->filearea  = $filerecord->filearea;
1203         $newrecord->itemid    = $filerecord->itemid;
1204         $newrecord->filepath  = $filerecord->filepath;
1205         $newrecord->filename  = $filerecord->filename;
1207         $newrecord->timecreated  = $filerecord->timecreated;
1208         $newrecord->timemodified = $filerecord->timemodified;
1209         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1210         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1211         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1212         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1213         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1214         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1215         $newrecord->sortorder    = $filerecord->sortorder;
1217         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1219         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1221         try {
1222             $newrecord->id = $DB->insert_record('files', $newrecord);
1223         } catch (dml_exception $e) {
1224             if ($newfile) {
1225                 $this->deleted_file_cleanup($newrecord->contenthash);
1226             }
1227             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1228                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1229         }
1231         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1233         return $this->get_file_instance($newrecord);
1234     }
1236     /**
1237      * Add new local file.
1238      *
1239      * @param stdClass|array $filerecord object or array describing file
1240      * @param string $content content of file
1241      * @return stored_file
1242      */
1243     public function create_file_from_string($filerecord, $content) {
1244         global $DB;
1246         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1247         $filerecord = (object)$filerecord; // We support arrays too.
1249         // validate all parameters, we do not want any rubbish stored in database, right?
1250         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1251             throw new file_exception('storedfileproblem', 'Invalid contextid');
1252         }
1254         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1255         if (empty($filerecord->component)) {
1256             throw new file_exception('storedfileproblem', 'Invalid component');
1257         }
1259         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1260         if (empty($filerecord->filearea)) {
1261             throw new file_exception('storedfileproblem', 'Invalid filearea');
1262         }
1264         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1265             throw new file_exception('storedfileproblem', 'Invalid itemid');
1266         }
1268         if (!empty($filerecord->sortorder)) {
1269             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1270                 $filerecord->sortorder = 0;
1271             }
1272         } else {
1273             $filerecord->sortorder = 0;
1274         }
1276         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1277         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1278             // path must start and end with '/'
1279             throw new file_exception('storedfileproblem', 'Invalid file path');
1280         }
1282         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1283         if ($filerecord->filename === '') {
1284             // path must start and end with '/'
1285             throw new file_exception('storedfileproblem', 'Invalid file name');
1286         }
1288         $now = time();
1289         if (isset($filerecord->timecreated)) {
1290             if (!is_number($filerecord->timecreated)) {
1291                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1292             }
1293             if ($filerecord->timecreated < 0) {
1294                 //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)
1295                 $filerecord->timecreated = 0;
1296             }
1297         } else {
1298             $filerecord->timecreated = $now;
1299         }
1301         if (isset($filerecord->timemodified)) {
1302             if (!is_number($filerecord->timemodified)) {
1303                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1304             }
1305             if ($filerecord->timemodified < 0) {
1306                 //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)
1307                 $filerecord->timemodified = 0;
1308             }
1309         } else {
1310             $filerecord->timemodified = $now;
1311         }
1313         $newrecord = new stdClass();
1315         $newrecord->contextid = $filerecord->contextid;
1316         $newrecord->component = $filerecord->component;
1317         $newrecord->filearea  = $filerecord->filearea;
1318         $newrecord->itemid    = $filerecord->itemid;
1319         $newrecord->filepath  = $filerecord->filepath;
1320         $newrecord->filename  = $filerecord->filename;
1322         $newrecord->timecreated  = $filerecord->timecreated;
1323         $newrecord->timemodified = $filerecord->timemodified;
1324         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1325         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1326         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1327         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1328         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1329         $newrecord->sortorder    = $filerecord->sortorder;
1331         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1332         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1333         // get mimetype by magic bytes
1334         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1336         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1338         try {
1339             $newrecord->id = $DB->insert_record('files', $newrecord);
1340         } catch (dml_exception $e) {
1341             if ($newfile) {
1342                 $this->deleted_file_cleanup($newrecord->contenthash);
1343             }
1344             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1345                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1346         }
1348         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1350         return $this->get_file_instance($newrecord);
1351     }
1353     /**
1354      * Create a new alias/shortcut file from file reference information
1355      *
1356      * @param stdClass|array $filerecord object or array describing the new file
1357      * @param int $repositoryid the id of the repository that provides the original file
1358      * @param string $reference the information required by the repository to locate the original file
1359      * @param array $options options for creating the new file
1360      * @return stored_file
1361      */
1362     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1363         global $DB;
1365         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1366         $filerecord = (object)$filerecord; // We support arrays too.
1368         // validate all parameters, we do not want any rubbish stored in database, right?
1369         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1370             throw new file_exception('storedfileproblem', 'Invalid contextid');
1371         }
1373         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1374         if (empty($filerecord->component)) {
1375             throw new file_exception('storedfileproblem', 'Invalid component');
1376         }
1378         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1379         if (empty($filerecord->filearea)) {
1380             throw new file_exception('storedfileproblem', 'Invalid filearea');
1381         }
1383         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1384             throw new file_exception('storedfileproblem', 'Invalid itemid');
1385         }
1387         if (!empty($filerecord->sortorder)) {
1388             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1389                 $filerecord->sortorder = 0;
1390             }
1391         } else {
1392             $filerecord->sortorder = 0;
1393         }
1395         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1396         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1397         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1398         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1399         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1400         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1401         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1402         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1403             // Path must start and end with '/'.
1404             throw new file_exception('storedfileproblem', 'Invalid file path');
1405         }
1407         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1408         if ($filerecord->filename === '') {
1409             // Path must start and end with '/'.
1410             throw new file_exception('storedfileproblem', 'Invalid file name');
1411         }
1413         $now = time();
1414         if (isset($filerecord->timecreated)) {
1415             if (!is_number($filerecord->timecreated)) {
1416                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1417             }
1418             if ($filerecord->timecreated < 0) {
1419                 // 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)
1420                 $filerecord->timecreated = 0;
1421             }
1422         } else {
1423             $filerecord->timecreated = $now;
1424         }
1426         if (isset($filerecord->timemodified)) {
1427             if (!is_number($filerecord->timemodified)) {
1428                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1429             }
1430             if ($filerecord->timemodified < 0) {
1431                 // 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)
1432                 $filerecord->timemodified = 0;
1433             }
1434         } else {
1435             $filerecord->timemodified = $now;
1436         }
1438         $transaction = $DB->start_delegated_transaction();
1440         try {
1441             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1442         } catch (Exception $e) {
1443             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1444         }
1446         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1447             // there was specified the contenthash for a file already stored in moodle filepool
1448             if (empty($filerecord->filesize)) {
1449                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1450                 $filerecord->filesize = filesize($filepathname);
1451             } else {
1452                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1453             }
1454         } else {
1455             // atempt to get the result of last synchronisation for this reference
1456             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1457                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1458             if ($lastcontent) {
1459                 $filerecord->contenthash = $lastcontent->contenthash;
1460                 $filerecord->filesize = $lastcontent->filesize;
1461             } else {
1462                 // External file doesn't have content in moodle.
1463                 // So we create an empty file for it.
1464                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1465             }
1466         }
1468         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1470         try {
1471             $filerecord->id = $DB->insert_record('files', $filerecord);
1472         } catch (dml_exception $e) {
1473             if (!empty($newfile)) {
1474                 $this->deleted_file_cleanup($filerecord->contenthash);
1475             }
1476             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1477                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1478         }
1480         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1482         $transaction->allow_commit();
1484         // this will retrieve all reference information from DB as well
1485         return $this->get_file_by_id($filerecord->id);
1486     }
1488     /**
1489      * Creates new image file from existing.
1490      *
1491      * @param stdClass|array $filerecord object or array describing new file
1492      * @param int|stored_file $fid file id or stored file object
1493      * @param int $newwidth in pixels
1494      * @param int $newheight in pixels
1495      * @param bool $keepaspectratio whether or not keep aspect ratio
1496      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1497      * @return stored_file
1498      */
1499     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1500         if (!function_exists('imagecreatefromstring')) {
1501             //Most likely the GD php extension isn't installed
1502             //image conversion cannot succeed
1503             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1504         }
1506         if ($fid instanceof stored_file) {
1507             $fid = $fid->get_id();
1508         }
1510         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1512         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1513             throw new file_exception('storedfileproblem', 'File does not exist');
1514         }
1516         if (!$imageinfo = $file->get_imageinfo()) {
1517             throw new file_exception('storedfileproblem', 'File is not an image');
1518         }
1520         if (!isset($filerecord['filename'])) {
1521             $filerecord['filename'] = $file->get_filename();
1522         }
1524         if (!isset($filerecord['mimetype'])) {
1525             $filerecord['mimetype'] = $imageinfo['mimetype'];
1526         }
1528         $width    = $imageinfo['width'];
1529         $height   = $imageinfo['height'];
1530         $mimetype = $imageinfo['mimetype'];
1532         if ($keepaspectratio) {
1533             if (0 >= $newwidth and 0 >= $newheight) {
1534                 // no sizes specified
1535                 $newwidth  = $width;
1536                 $newheight = $height;
1538             } else if (0 < $newwidth and 0 < $newheight) {
1539                 $xheight = ($newwidth*($height/$width));
1540                 if ($xheight < $newheight) {
1541                     $newheight = (int)$xheight;
1542                 } else {
1543                     $newwidth = (int)($newheight*($width/$height));
1544                 }
1546             } else if (0 < $newwidth) {
1547                 $newheight = (int)($newwidth*($height/$width));
1549             } else { //0 < $newheight
1550                 $newwidth = (int)($newheight*($width/$height));
1551             }
1553         } else {
1554             if (0 >= $newwidth) {
1555                 $newwidth = $width;
1556             }
1557             if (0 >= $newheight) {
1558                 $newheight = $height;
1559             }
1560         }
1562         $img = imagecreatefromstring($file->get_content());
1563         if ($height != $newheight or $width != $newwidth) {
1564             $newimg = imagecreatetruecolor($newwidth, $newheight);
1565             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1566                 // weird
1567                 throw new file_exception('storedfileproblem', 'Can not resize image');
1568             }
1569             imagedestroy($img);
1570             $img = $newimg;
1571         }
1573         ob_start();
1574         switch ($filerecord['mimetype']) {
1575             case 'image/gif':
1576                 imagegif($img);
1577                 break;
1579             case 'image/jpeg':
1580                 if (is_null($quality)) {
1581                     imagejpeg($img);
1582                 } else {
1583                     imagejpeg($img, NULL, $quality);
1584                 }
1585                 break;
1587             case 'image/png':
1588                 $quality = (int)$quality;
1589                 imagepng($img, NULL, $quality, NULL);
1590                 break;
1592             default:
1593                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1594         }
1596         $content = ob_get_contents();
1597         ob_end_clean();
1598         imagedestroy($img);
1600         if (!$content) {
1601             throw new file_exception('storedfileproblem', 'Can not convert image');
1602         }
1604         return $this->create_file_from_string($filerecord, $content);
1605     }
1607     /**
1608      * Add file content to sha1 pool.
1609      *
1610      * @param string $pathname path to file
1611      * @param string $contenthash sha1 hash of content if known (performance only)
1612      * @return array (contenthash, filesize, newfile)
1613      */
1614     public function add_file_to_pool($pathname, $contenthash = NULL) {
1615         global $CFG;
1617         if (!is_readable($pathname)) {
1618             throw new file_exception('storedfilecannotread', '', $pathname);
1619         }
1621         $filesize = filesize($pathname);
1622         if ($filesize === false) {
1623             throw new file_exception('storedfilecannotread', '', $pathname);
1624         }
1626         if (is_null($contenthash)) {
1627             $contenthash = sha1_file($pathname);
1628         } else if ($CFG->debugdeveloper) {
1629             $filehash = sha1_file($pathname);
1630             if ($filehash === false) {
1631                 throw new file_exception('storedfilecannotread', '', $pathname);
1632             }
1633             if ($filehash !== $contenthash) {
1634                 // Hopefully this never happens, if yes we need to fix calling code.
1635                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1636                 $contenthash = $filehash;
1637             }
1638         }
1639         if ($contenthash === false) {
1640             throw new file_exception('storedfilecannotread', '', $pathname);
1641         }
1643         if ($filesize > 0 and $contenthash === sha1('')) {
1644             // Did the file change or is sha1_file() borked for this file?
1645             clearstatcache();
1646             $contenthash = sha1_file($pathname);
1647             $filesize = filesize($pathname);
1649             if ($contenthash === false or $filesize === false) {
1650                 throw new file_exception('storedfilecannotread', '', $pathname);
1651             }
1652             if ($filesize > 0 and $contenthash === sha1('')) {
1653                 // This is very weird...
1654                 throw new file_exception('storedfilecannotread', '', $pathname);
1655             }
1656         }
1658         $hashpath = $this->path_from_hash($contenthash);
1659         $hashfile = "$hashpath/$contenthash";
1661         $newfile = true;
1663         if (file_exists($hashfile)) {
1664             if (filesize($hashfile) === $filesize) {
1665                 return array($contenthash, $filesize, false);
1666             }
1667             if (sha1_file($hashfile) === $contenthash) {
1668                 // Jackpot! We have a sha1 collision.
1669                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1670                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1671                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1672                 throw new file_pool_content_exception($contenthash);
1673             }
1674             debugging("Replacing invalid content file $contenthash");
1675             unlink($hashfile);
1676             $newfile = false;
1677         }
1679         if (!is_dir($hashpath)) {
1680             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1681                 // Permission trouble.
1682                 throw new file_exception('storedfilecannotcreatefiledirs');
1683             }
1684         }
1686         // Let's try to prevent some race conditions.
1688         $prev = ignore_user_abort(true);
1689         @unlink($hashfile.'.tmp');
1690         if (!copy($pathname, $hashfile.'.tmp')) {
1691             // Borked permissions or out of disk space.
1692             ignore_user_abort($prev);
1693             throw new file_exception('storedfilecannotcreatefile');
1694         }
1695         if (filesize($hashfile.'.tmp') !== $filesize) {
1696             // This should not happen.
1697             unlink($hashfile.'.tmp');
1698             ignore_user_abort($prev);
1699             throw new file_exception('storedfilecannotcreatefile');
1700         }
1701         rename($hashfile.'.tmp', $hashfile);
1702         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1703         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1704         ignore_user_abort($prev);
1706         return array($contenthash, $filesize, $newfile);
1707     }
1709     /**
1710      * Add string content to sha1 pool.
1711      *
1712      * @param string $content file content - binary string
1713      * @return array (contenthash, filesize, newfile)
1714      */
1715     public function add_string_to_pool($content) {
1716         global $CFG;
1718         $contenthash = sha1($content);
1719         $filesize = strlen($content); // binary length
1721         $hashpath = $this->path_from_hash($contenthash);
1722         $hashfile = "$hashpath/$contenthash";
1724         $newfile = true;
1726         if (file_exists($hashfile)) {
1727             if (filesize($hashfile) === $filesize) {
1728                 return array($contenthash, $filesize, false);
1729             }
1730             if (sha1_file($hashfile) === $contenthash) {
1731                 // Jackpot! We have a sha1 collision.
1732                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1733                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
1734                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
1735                 throw new file_pool_content_exception($contenthash);
1736             }
1737             debugging("Replacing invalid content file $contenthash");
1738             unlink($hashfile);
1739             $newfile = false;
1740         }
1742         if (!is_dir($hashpath)) {
1743             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1744                 // Permission trouble.
1745                 throw new file_exception('storedfilecannotcreatefiledirs');
1746             }
1747         }
1749         // Hopefully this works around most potential race conditions.
1751         $prev = ignore_user_abort(true);
1753         if (!empty($CFG->preventfilelocking)) {
1754             $newsize = file_put_contents($hashfile.'.tmp', $content);
1755         } else {
1756             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
1757         }
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      * Search through the server files.
1959      *
1960      * The query parameter will be used in conjuction with the SQL directive
1961      * LIKE, so include '%' in it if you need to. This search will always ignore
1962      * user files and directories. Note that the search is case insensitive.
1963      *
1964      * This query can quickly become inefficient so use it sparignly.
1965      *
1966      * @param  string  $query The string used with SQL LIKE.
1967      * @param  integer $from  The offset to start the search at.
1968      * @param  integer $limit The maximum number of results.
1969      * @param  boolean $count When true this methods returns the number of results availabe,
1970      *                        disregarding the parameters $from and $limit.
1971      * @return int|array      Integer when count, otherwise array of stored_file objects.
1972      */
1973     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
1974         global $DB;
1975         $params = array(
1976             'contextlevel' => CONTEXT_USER,
1977             'directory' => '.',
1978             'query' => $query
1979         );
1981         if ($count) {
1982             $select = 'COUNT(1)';
1983         } else {
1984             $select = self::instance_sql_fields('f', 'r');
1985         }
1986         $like = $DB->sql_like('f.filename', ':query', false);
1988         $sql = "SELECT $select
1989                   FROM {files} f
1990              LEFT JOIN {files_reference} r
1991                     ON f.referencefileid = r.id
1992                   JOIN {context} c
1993                     ON f.contextid = c.id
1994                  WHERE c.contextlevel <> :contextlevel
1995                    AND f.filename <> :directory
1996                    AND " . $like . "";
1998         if ($count) {
1999             return $DB->count_records_sql($sql, $params);
2000         }
2002         $sql .= " ORDER BY f.filename";
2004         $result = array();
2005         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2006         foreach ($filerecords as $filerecord) {
2007             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2008         }
2009         $filerecords->close();
2011         return $result;
2012     }
2014     /**
2015      * Returns all aliases that refer to some stored_file via the given reference
2016      *
2017      * All repositories that provide access to a stored_file are expected to use
2018      * {@link self::pack_reference()}. This method can't be used if the given reference
2019      * does not use this format or if you are looking for references to an external file
2020      * (for example it can't be used to search for all aliases that refer to a given
2021      * Dropbox or Box.net file).
2022      *
2023      * Aliases in user draft areas are excluded from the returned list.
2024      *
2025      * @param string $reference identification of the referenced file
2026      * @return array of stored_file indexed by its pathnamehash
2027      */
2028     public function search_references($reference) {
2029         global $DB;
2031         if (is_null($reference)) {
2032             throw new coding_exception('NULL is not a valid reference to an external file');
2033         }
2035         // Give {@link self::unpack_reference()} a chance to throw exception if the
2036         // reference is not in a valid format.
2037         self::unpack_reference($reference);
2039         $referencehash = sha1($reference);
2041         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2042                   FROM {files} f
2043                   JOIN {files_reference} r ON f.referencefileid = r.id
2044                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2045                  WHERE r.referencehash = ?
2046                        AND (f.component <> ? OR f.filearea <> ?)";
2048         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2049         $files = array();
2050         foreach ($rs as $filerecord) {
2051             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2052         }
2054         return $files;
2055     }
2057     /**
2058      * Returns the number of aliases that refer to some stored_file via the given reference
2059      *
2060      * All repositories that provide access to a stored_file are expected to use
2061      * {@link self::pack_reference()}. This method can't be used if the given reference
2062      * does not use this format or if you are looking for references to an external file
2063      * (for example it can't be used to count aliases that refer to a given Dropbox or
2064      * Box.net file).
2065      *
2066      * Aliases in user draft areas are not counted.
2067      *
2068      * @param string $reference identification of the referenced file
2069      * @return int
2070      */
2071     public function search_references_count($reference) {
2072         global $DB;
2074         if (is_null($reference)) {
2075             throw new coding_exception('NULL is not a valid reference to an external file');
2076         }
2078         // Give {@link self::unpack_reference()} a chance to throw exception if the
2079         // reference is not in a valid format.
2080         self::unpack_reference($reference);
2082         $referencehash = sha1($reference);
2084         $sql = "SELECT COUNT(f.id)
2085                   FROM {files} f
2086                   JOIN {files_reference} r ON f.referencefileid = r.id
2087                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2088                  WHERE r.referencehash = ?
2089                        AND (f.component <> ? OR f.filearea <> ?)";
2091         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2092     }
2094     /**
2095      * Returns all aliases that link to the given stored_file
2096      *
2097      * Aliases in user draft areas are excluded from the returned list.
2098      *
2099      * @param stored_file $storedfile
2100      * @return array of stored_file
2101      */
2102     public function get_references_by_storedfile(stored_file $storedfile) {
2103         global $DB;
2105         $params = array();
2106         $params['contextid'] = $storedfile->get_contextid();
2107         $params['component'] = $storedfile->get_component();
2108         $params['filearea']  = $storedfile->get_filearea();
2109         $params['itemid']    = $storedfile->get_itemid();
2110         $params['filename']  = $storedfile->get_filename();
2111         $params['filepath']  = $storedfile->get_filepath();
2113         return $this->search_references(self::pack_reference($params));
2114     }
2116     /**
2117      * Returns the number of aliases that link to the given stored_file
2118      *
2119      * Aliases in user draft areas are not counted.
2120      *
2121      * @param stored_file $storedfile
2122      * @return int
2123      */
2124     public function get_references_count_by_storedfile(stored_file $storedfile) {
2125         global $DB;
2127         $params = array();
2128         $params['contextid'] = $storedfile->get_contextid();
2129         $params['component'] = $storedfile->get_component();
2130         $params['filearea']  = $storedfile->get_filearea();
2131         $params['itemid']    = $storedfile->get_itemid();
2132         $params['filename']  = $storedfile->get_filename();
2133         $params['filepath']  = $storedfile->get_filepath();
2135         return $this->search_references_count(self::pack_reference($params));
2136     }
2138     /**
2139      * Updates all files that are referencing this file with the new contenthash
2140      * and filesize
2141      *
2142      * @param stored_file $storedfile
2143      */
2144     public function update_references_to_storedfile(stored_file $storedfile) {
2145         global $CFG, $DB;
2146         $params = array();
2147         $params['contextid'] = $storedfile->get_contextid();
2148         $params['component'] = $storedfile->get_component();
2149         $params['filearea']  = $storedfile->get_filearea();
2150         $params['itemid']    = $storedfile->get_itemid();
2151         $params['filename']  = $storedfile->get_filename();
2152         $params['filepath']  = $storedfile->get_filepath();
2153         $reference = self::pack_reference($params);
2154         $referencehash = sha1($reference);
2156         $sql = "SELECT repositoryid, id FROM {files_reference}
2157                  WHERE referencehash = ?";
2158         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2160         $now = time();
2161         foreach ($rs as $record) {
2162             $this->update_references($record->id, $now, null,
2163                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
2164         }
2165         $rs->close();
2166     }
2168     /**
2169      * Convert file alias to local file
2170      *
2171      * @throws moodle_exception if file could not be downloaded
2172      *
2173      * @param stored_file $storedfile a stored_file instances
2174      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2175      * @return stored_file stored_file
2176      */
2177     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2178         global $CFG;
2179         $storedfile->import_external_file_contents($maxbytes);
2180         $storedfile->delete_reference();
2181         return $storedfile;
2182     }
2184     /**
2185      * Return mimetype by given file pathname
2186      *
2187      * If file has a known extension, we return the mimetype based on extension.
2188      * Otherwise (when possible) we try to get the mimetype from file contents.
2189      *
2190      * @param string $pathname full path to the file
2191      * @param string $filename correct file name with extension, if omitted will be taken from $path
2192      * @return string
2193      */
2194     public static function mimetype($pathname, $filename = null) {
2195         if (empty($filename)) {
2196             $filename = $pathname;
2197         }
2198         $type = mimeinfo('type', $filename);
2199         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2200             $finfo = new finfo(FILEINFO_MIME_TYPE);
2201             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2202         }
2203         return $type;
2204     }
2206     /**
2207      * Cron cleanup job.
2208      */
2209     public function cron() {
2210         global $CFG, $DB;
2211         require_once($CFG->libdir.'/cronlib.php');
2213         // find out all stale draft areas (older than 4 days) and purge them
2214         // those are identified by time stamp of the /. root dir
2215         mtrace('Deleting old draft files... ', '');
2216         cron_trace_time_and_memory();
2217         $old = time() - 60*60*24*4;
2218         $sql = "SELECT *
2219                   FROM {files}
2220                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2221                        AND timecreated < :old";
2222         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2223         foreach ($rs as $dir) {
2224             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2225         }
2226         $rs->close();
2227         mtrace('done.');
2229         // remove orphaned preview files (that is files in the core preview filearea without
2230         // the existing original file)
2231         mtrace('Deleting orphaned preview files... ', '');
2232         cron_trace_time_and_memory();
2233         $sql = "SELECT p.*
2234                   FROM {files} p
2235              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2236                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2237                        AND o.id IS NULL";
2238         $syscontext = context_system::instance();
2239         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2240         foreach ($rs as $orphan) {
2241             $file = $this->get_file_instance($orphan);
2242             if (!$file->is_directory()) {
2243                 $file->delete();
2244             }
2245         }
2246         $rs->close();
2247         mtrace('done.');
2249         // remove trash pool files once a day
2250         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2251         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2252             require_once($CFG->libdir.'/filelib.php');
2253             // Delete files that are associated with a context that no longer exists.
2254             mtrace('Cleaning up files from deleted contexts... ', '');
2255             cron_trace_time_and_memory();
2256             $sql = "SELECT DISTINCT f.contextid
2257                     FROM {files} f
2258                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2259                     WHERE c.id IS NULL";
2260             $rs = $DB->get_recordset_sql($sql);
2261             if ($rs->valid()) {
2262                 $fs = get_file_storage();
2263                 foreach ($rs as $ctx) {
2264                     $fs->delete_area_files($ctx->contextid);
2265                 }
2266             }
2267             $rs->close();
2268             mtrace('done.');
2270             mtrace('Deleting trash files... ', '');
2271             cron_trace_time_and_memory();
2272             fulldelete($this->trashdir);
2273             set_config('fileslastcleanup', time());
2274             mtrace('done.');
2275         }
2276     }
2278     /**
2279      * Get the sql formated fields for a file instance to be created from a
2280      * {files} and {files_refernece} join.
2281      *
2282      * @param string $filesprefix the table prefix for the {files} table
2283      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2284      * @return string the sql to go after a SELECT
2285      */
2286     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2287         // Note, these fieldnames MUST NOT overlap between the two tables,
2288         // else problems like MDL-33172 occur.
2289         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2290             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2291             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2293         $referencefields = array('repositoryid' => 'repositoryid',
2294             'reference' => 'reference',
2295             'lastsync' => 'referencelastsync');
2297         // id is specifically named to prevent overlaping between the two tables.
2298         $fields = array();
2299         $fields[] = $filesprefix.'.id AS id';
2300         foreach ($filefields as $field) {
2301             $fields[] = "{$filesprefix}.{$field}";
2302         }
2304         foreach ($referencefields as $field => $alias) {
2305             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2306         }
2308         return implode(', ', $fields);
2309     }
2311     /**
2312      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2313      *
2314      * If the record already exists, its id is returned. If there is no such record yet,
2315      * new one is created (using the lastsync provided, too) and its id is returned.
2316      *
2317      * @param int $repositoryid
2318      * @param string $reference
2319      * @param int $lastsync
2320      * @param int $lifetime argument not used any more
2321      * @return int
2322      */
2323     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2324         global $DB;
2326         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2328         if ($id !== false) {
2329             // bah, that was easy
2330             return $id;
2331         }
2333         // no such record yet, create one
2334         try {
2335             $id = $DB->insert_record('files_reference', array(
2336                 'repositoryid'  => $repositoryid,
2337                 'reference'     => $reference,
2338                 'referencehash' => sha1($reference),
2339                 'lastsync'      => $lastsync));
2340         } catch (dml_exception $e) {
2341             // if inserting the new record failed, chances are that the race condition has just
2342             // occured and the unique index did not allow to create the second record with the same
2343             // repositoryid + reference combo
2344             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2345         }
2347         return $id;
2348     }
2350     /**
2351      * Returns the id of the record in {files_reference} that matches the passed parameters
2352      *
2353      * Depending on the required strictness, false can be returned. The behaviour is consistent
2354      * with standard DML methods.
2355      *
2356      * @param int $repositoryid
2357      * @param string $reference
2358      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2359      * @return int|bool
2360      */
2361     private function get_referencefileid($repositoryid, $reference, $strictness) {
2362         global $DB;
2364         return $DB->get_field('files_reference', 'id',
2365             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2366     }
2368     /**
2369      * Updates a reference to the external resource and all files that use it
2370      *
2371      * This function is called after synchronisation of an external file and updates the
2372      * contenthash, filesize and status of all files that reference this external file
2373      * as well as time last synchronised.
2374      *
2375      * @param int $referencefileid
2376      * @param int $lastsync
2377      * @param int $lifetime argument not used any more, liefetime is returned by repository
2378      * @param string $contenthash
2379      * @param int $filesize
2380      * @param int $status 0 if ok or 666 if source is missing
2381      */
2382     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2383         global $DB;
2384         $referencefileid = clean_param($referencefileid, PARAM_INT);
2385         $lastsync = clean_param($lastsync, PARAM_INT);
2386         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2387         $filesize = clean_param($filesize, PARAM_INT);
2388         $status = clean_param($status, PARAM_INT);
2389         $params = array('contenthash' => $contenthash,
2390                     'filesize' => $filesize,
2391                     'status' => $status,
2392                     'referencefileid' => $referencefileid);
2393         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2394             status = :status
2395             WHERE referencefileid = :referencefileid', $params);
2396         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2397         $DB->update_record('files_reference', (object)$data);
2398     }