MDL-60318 filestorage: Fixed undefined method move_to_trash()
[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 {
47     /** @var string tempdir */
48     private $tempdir;
50     /** @var file_system filesystem */
51     private $filesystem;
53     /**
54      * Constructor - do not use directly use {@link get_file_storage()} call instead.
55      */
56     public function __construct() {
57         // The tempdir must always remain on disk, but shared between all ndoes in a cluster. Its content is not subject
58         // to the file_system abstraction.
59         $this->tempdir = make_temp_directory('filestorage');
61         $this->setup_file_system();
62     }
64     /**
65      * Complete setup procedure for the file_system component.
66      *
67      * @return file_system
68      */
69     public function setup_file_system() {
70         global $CFG;
71         if ($this->filesystem === null) {
72             require_once($CFG->libdir . '/filestorage/file_system.php');
73             if (!empty($CFG->alternative_file_system_class)) {
74                 $class = $CFG->alternative_file_system_class;
75             } else {
76                 // The default file_system is the filedir.
77                 require_once($CFG->libdir . '/filestorage/file_system_filedir.php');
78                 $class = file_system_filedir::class;
79             }
80             $this->filesystem = new $class();
81         }
83         return $this->filesystem;
84     }
86     /**
87      * Return the file system instance.
88      *
89      * @return file_system
90      */
91     public function get_file_system() {
92         return $this->filesystem;
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);
156         return $storedfile;
157     }
159     /**
160      * Get converted document.
161      *
162      * Get an alternate version of the specified document, if it is possible to convert.
163      *
164      * @param stored_file $file the file we want to preview
165      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
166      * @param boolean $forcerefresh If true, the file will be converted every time (not cached).
167      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
168      */
169     public function get_converted_document(stored_file $file, $format, $forcerefresh = false) {
170         debugging('The get_converted_document function has been deprecated and the unoconv functions been removed. '
171                 . 'The file has not been converted. '
172                 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
174         return false;
175     }
177     /**
178      * Verify the format is supported.
179      *
180      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
181      * @return bool - True if the format is supported for input.
182      */
183     protected function is_format_supported_by_unoconv($format) {
184         debugging('The is_format_supported_by_unoconv function has been deprecated and the unoconv functions been removed. '
185                 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
187         return false;
188     }
190     /**
191      * Check if the installed version of unoconv is supported.
192      *
193      * @return bool true if the present version is supported, false otherwise.
194      */
195     public static function can_convert_documents() {
196         debugging('The can_convert_documents function has been deprecated and the unoconv functions been removed. '
197                 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
199         return false;
200     }
202     /**
203      * Regenerate the test pdf and send it direct to the browser.
204      */
205     public static function send_test_pdf() {
206         debugging('The send_test_pdf function has been deprecated and the unoconv functions been removed. '
207                 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
209         return false;
210     }
212     /**
213      * Check if unoconv configured path is correct and working.
214      *
215      * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
216      */
217     public static function test_unoconv_path() {
218         debugging('The test_unoconv_path function has been deprecated and the unoconv functions been removed. '
219                 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER);
221         return false;
222     }
224     /**
225      * Returns an image file that represent the given stored file as a preview
226      *
227      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
228      * future, the support for other mimetypes can be added, too (eg. generate an image
229      * preview of PDF, text documents etc).
230      *
231      * @param stored_file $file the file we want to preview
232      * @param string $mode preview mode, eg. 'thumb'
233      * @return stored_file|bool false if unable to create the preview, stored file otherwise
234      */
235     public function get_file_preview(stored_file $file, $mode) {
237         $context = context_system::instance();
238         $path = '/' . trim($mode, '/') . '/';
239         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
241         if (!$preview) {
242             $preview = $this->create_file_preview($file, $mode);
243             if (!$preview) {
244                 return false;
245             }
246         }
248         return $preview;
249     }
251     /**
252      * Return an available file name.
253      *
254      * This will return the next available file name in the area, adding/incrementing a suffix
255      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
256      *
257      * If the file name passed is available without modification, it is returned as is.
258      *
259      * @param int $contextid context ID.
260      * @param string $component component.
261      * @param string $filearea file area.
262      * @param int $itemid area item ID.
263      * @param string $filepath the file path.
264      * @param string $filename the file name.
265      * @return string available file name.
266      * @throws coding_exception if the file name is invalid.
267      * @since Moodle 2.5
268      */
269     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
270         global $DB;
272         // Do not accept '.' or an empty file name (zero is acceptable).
273         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
274             throw new coding_exception('Invalid file name passed', $filename);
275         }
277         // The file does not exist, we return the same file name.
278         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
279             return $filename;
280         }
282         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
283         $pathinfo = pathinfo($filename);
284         $basename = $pathinfo['filename'];
285         $matches = array();
286         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
287             $basename = $matches[1];
288         }
290         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
291         if (isset($pathinfo['extension'])) {
292             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
293         }
295         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
296         $filenamelen = $DB->sql_length('f.filename');
297         $sql = "SELECT filename
298                 FROM {files} f
299                 WHERE
300                     f.contextid = :contextid AND
301                     f.component = :component AND
302                     f.filearea = :filearea AND
303                     f.itemid = :itemid AND
304                     f.filepath = :filepath AND
305                     $filenamelikesql
306                 ORDER BY
307                     $filenamelen DESC,
308                     f.filename DESC";
309         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
310                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
311         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
313         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
314         // would both be returned, but only the one only containing digits should be used.
315         $number = 1;
316         foreach ($results as $result) {
317             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
318             $matches = array();
319             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
320                 $number = $matches[2] + 1;
321                 break;
322             }
323         }
325         // Constructing the new filename.
326         $newfilename = $basename . ' (' . $number . ')';
327         if (isset($pathinfo['extension'])) {
328             $newfilename .= '.' . $pathinfo['extension'];
329         }
331         return $newfilename;
332     }
334     /**
335      * Return an available directory name.
336      *
337      * This will return the next available directory name in the area, adding/incrementing a suffix
338      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
339      *
340      * If the file path passed is available without modification, it is returned as is.
341      *
342      * @param int $contextid context ID.
343      * @param string $component component.
344      * @param string $filearea file area.
345      * @param int $itemid area item ID.
346      * @param string $suggestedpath the suggested file path.
347      * @return string available file path
348      * @since Moodle 2.5
349      */
350     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
351         global $DB;
353         // Ensure suggestedpath has trailing '/'
354         $suggestedpath = rtrim($suggestedpath, '/'). '/';
356         // The directory does not exist, we return the same file path.
357         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
358             return $suggestedpath;
359         }
361         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
362         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
363             $suggestedpath = $matches[1]. '/';
364         }
366         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
368         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
369         $filepathlen = $DB->sql_length('f.filepath');
370         $sql = "SELECT filepath
371                 FROM {files} f
372                 WHERE
373                     f.contextid = :contextid AND
374                     f.component = :component AND
375                     f.filearea = :filearea AND
376                     f.itemid = :itemid AND
377                     f.filename = :filename AND
378                     $filepathlikesql
379                 ORDER BY
380                     $filepathlen DESC,
381                     f.filepath DESC";
382         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
383                 'filename' => '.', 'filepathlike' => $filepathlike);
384         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
386         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
387         // would both be returned, but only the one only containing digits should be used.
388         $number = 1;
389         foreach ($results as $result) {
390             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
391                 $number = (int)($matches[1]) + 1;
392                 break;
393             }
394         }
396         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
397     }
399     /**
400      * Generates a preview image for the stored file
401      *
402      * @param stored_file $file the file we want to preview
403      * @param string $mode preview mode, eg. 'thumb'
404      * @return stored_file|bool the newly created preview file or false
405      */
406     protected function create_file_preview(stored_file $file, $mode) {
408         $mimetype = $file->get_mimetype();
410         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
411             // make a preview of the image
412             $data = $this->create_imagefile_preview($file, $mode);
414         } else {
415             // unable to create the preview of this mimetype yet
416             return false;
417         }
419         if (empty($data)) {
420             return false;
421         }
423         $context = context_system::instance();
424         $record = array(
425             'contextid' => $context->id,
426             'component' => 'core',
427             'filearea'  => 'preview',
428             'itemid'    => 0,
429             'filepath'  => '/' . trim($mode, '/') . '/',
430             'filename'  => $file->get_contenthash(),
431         );
433         $imageinfo = getimagesizefromstring($data);
434         if ($imageinfo) {
435             $record['mimetype'] = $imageinfo['mime'];
436         }
438         return $this->create_file_from_string($record, $data);
439     }
441     /**
442      * Generates a preview for the stored image file
443      *
444      * @param stored_file $file the image we want to preview
445      * @param string $mode preview mode, eg. 'thumb'
446      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
447      */
448     protected function create_imagefile_preview(stored_file $file, $mode) {
449         global $CFG;
450         require_once($CFG->libdir.'/gdlib.php');
452         if ($mode === 'tinyicon') {
453             $data = $file->generate_image_thumbnail(24, 24);
455         } else if ($mode === 'thumb') {
456             $data = $file->generate_image_thumbnail(90, 90);
458         } else if ($mode === 'bigthumb') {
459             $data = $file->generate_image_thumbnail(250, 250);
461         } else {
462             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
463         }
465         return $data;
466     }
468     /**
469      * Fetch file using local file id.
470      *
471      * Please do not rely on file ids, it is usually easier to use
472      * pathname hashes instead.
473      *
474      * @param int $fileid file ID
475      * @return stored_file|bool stored_file instance if exists, false if not
476      */
477     public function get_file_by_id($fileid) {
478         global $DB;
480         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
481                   FROM {files} f
482              LEFT JOIN {files_reference} r
483                        ON f.referencefileid = r.id
484                  WHERE f.id = ?";
485         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
486             return $this->get_file_instance($filerecord);
487         } else {
488             return false;
489         }
490     }
492     /**
493      * Fetch file using local file full pathname hash
494      *
495      * @param string $pathnamehash path name hash
496      * @return stored_file|bool stored_file instance if exists, false if not
497      */
498     public function get_file_by_hash($pathnamehash) {
499         global $DB;
501         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
502                   FROM {files} f
503              LEFT JOIN {files_reference} r
504                        ON f.referencefileid = r.id
505                  WHERE f.pathnamehash = ?";
506         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
507             return $this->get_file_instance($filerecord);
508         } else {
509             return false;
510         }
511     }
513     /**
514      * Fetch locally stored file.
515      *
516      * @param int $contextid context ID
517      * @param string $component component
518      * @param string $filearea file area
519      * @param int $itemid item ID
520      * @param string $filepath file path
521      * @param string $filename file name
522      * @return stored_file|bool stored_file instance if exists, false if not
523      */
524     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
525         $filepath = clean_param($filepath, PARAM_PATH);
526         $filename = clean_param($filename, PARAM_FILE);
528         if ($filename === '') {
529             $filename = '.';
530         }
532         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
533         return $this->get_file_by_hash($pathnamehash);
534     }
536     /**
537      * Are there any files (or directories)
538      *
539      * @param int $contextid context ID
540      * @param string $component component
541      * @param string $filearea file area
542      * @param bool|int $itemid item id or false if all items
543      * @param bool $ignoredirs whether or not ignore directories
544      * @return bool empty
545      */
546     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
547         global $DB;
549         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
550         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
552         if ($itemid !== false) {
553             $params['itemid'] = $itemid;
554             $where .= " AND itemid = :itemid";
555         }
557         if ($ignoredirs) {
558             $sql = "SELECT 'x'
559                       FROM {files}
560                      WHERE $where AND filename <> '.'";
561         } else {
562             $sql = "SELECT 'x'
563                       FROM {files}
564                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
565         }
567         return !$DB->record_exists_sql($sql, $params);
568     }
570     /**
571      * Returns all files belonging to given repository
572      *
573      * @param int $repositoryid
574      * @param string $sort A fragment of SQL to use for sorting
575      */
576     public function get_external_files($repositoryid, $sort = '') {
577         global $DB;
578         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
579                   FROM {files} f
580              LEFT JOIN {files_reference} r
581                        ON f.referencefileid = r.id
582                  WHERE r.repositoryid = ?";
583         if (!empty($sort)) {
584             $sql .= " ORDER BY {$sort}";
585         }
587         $result = array();
588         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
589         foreach ($filerecords as $filerecord) {
590             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
591         }
592         return $result;
593     }
595     /**
596      * Returns all area files (optionally limited by itemid)
597      *
598      * @param int $contextid context ID
599      * @param string $component component
600      * @param mixed $filearea file area/s, you cannot specify multiple fileareas as well as an itemid
601      * @param int $itemid item ID or all files if not specified
602      * @param string $sort A fragment of SQL to use for sorting
603      * @param bool $includedirs whether or not include directories
604      * @param int $updatedsince return files updated since this time
605      * @param int $limitfrom return a subset of records, starting at this point (optional).
606      * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
607      * @return stored_file[] array of stored_files indexed by pathanmehash
608      */
609     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename",
610             $includedirs = true, $updatedsince = 0, $limitfrom = 0, $limitnum = 0) {
611         global $DB;
613         list($areasql, $conditions) = $DB->get_in_or_equal($filearea, SQL_PARAMS_NAMED);
614         $conditions['contextid'] = $contextid;
615         $conditions['component'] = $component;
617         if ($itemid !== false && is_array($filearea)) {
618             throw new coding_exception('You cannot specify multiple fileareas as well as an itemid.');
619         } else if ($itemid !== false) {
620             $itemidsql = ' AND f.itemid = :itemid ';
621             $conditions['itemid'] = $itemid;
622         } else {
623             $itemidsql = '';
624         }
626         $updatedsincesql = '';
627         if (!empty($updatedsince)) {
628             $conditions['time'] = $updatedsince;
629             $updatedsincesql = 'AND f.timemodified > :time';
630         }
632         $includedirssql = '';
633         if (!$includedirs) {
634             $includedirssql = 'AND f.filename != :dot';
635             $conditions['dot'] = '.';
636         }
638         if ($limitfrom && !$limitnum) {
639             throw new coding_exception('If specifying $limitfrom you must also specify $limitnum');
640         }
642         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
643                   FROM {files} f
644              LEFT JOIN {files_reference} r
645                        ON f.referencefileid = r.id
646                  WHERE f.contextid = :contextid
647                        AND f.component = :component
648                        AND f.filearea $areasql
649                        $includedirssql
650                        $updatedsincesql
651                        $itemidsql";
652         if (!empty($sort)) {
653             $sql .= " ORDER BY {$sort}";
654         }
656         $result = array();
657         $filerecords = $DB->get_records_sql($sql, $conditions, $limitfrom, $limitnum);
658         foreach ($filerecords as $filerecord) {
659             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
660         }
661         return $result;
662     }
664     /**
665      * Returns array based tree structure of area files
666      *
667      * @param int $contextid context ID
668      * @param string $component component
669      * @param string $filearea file area
670      * @param int $itemid item ID
671      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
672      */
673     public function get_area_tree($contextid, $component, $filearea, $itemid) {
674         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
675         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
676         // first create directory structure
677         foreach ($files as $hash=>$dir) {
678             if (!$dir->is_directory()) {
679                 continue;
680             }
681             unset($files[$hash]);
682             if ($dir->get_filepath() === '/') {
683                 $result['dirfile'] = $dir;
684                 continue;
685             }
686             $parts = explode('/', trim($dir->get_filepath(),'/'));
687             $pointer =& $result;
688             foreach ($parts as $part) {
689                 if ($part === '') {
690                     continue;
691                 }
692                 if (!isset($pointer['subdirs'][$part])) {
693                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
694                 }
695                 $pointer =& $pointer['subdirs'][$part];
696             }
697             $pointer['dirfile'] = $dir;
698             unset($pointer);
699         }
700         foreach ($files as $hash=>$file) {
701             $parts = explode('/', trim($file->get_filepath(),'/'));
702             $pointer =& $result;
703             foreach ($parts as $part) {
704                 if ($part === '') {
705                     continue;
706                 }
707                 $pointer =& $pointer['subdirs'][$part];
708             }
709             $pointer['files'][$file->get_filename()] = $file;
710             unset($pointer);
711         }
712         $result = $this->sort_area_tree($result);
713         return $result;
714     }
716     /**
717      * Sorts the result of {@link file_storage::get_area_tree()}.
718      *
719      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
720      * @return array of sorted results
721      */
722     protected function sort_area_tree($tree) {
723         foreach ($tree as $key => &$value) {
724             if ($key == 'subdirs') {
725                 core_collator::ksort($value, core_collator::SORT_NATURAL);
726                 foreach ($value as $subdirname => &$subtree) {
727                     $subtree = $this->sort_area_tree($subtree);
728                 }
729             } else if ($key == 'files') {
730                 core_collator::ksort($value, core_collator::SORT_NATURAL);
731             }
732         }
733         return $tree;
734     }
736     /**
737      * Returns all files and optionally directories
738      *
739      * @param int $contextid context ID
740      * @param string $component component
741      * @param string $filearea file area
742      * @param int $itemid item ID
743      * @param int $filepath directory path
744      * @param bool $recursive include all subdirectories
745      * @param bool $includedirs include files and directories
746      * @param string $sort A fragment of SQL to use for sorting
747      * @return array of stored_files indexed by pathanmehash
748      */
749     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
750         global $DB;
752         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
753             return array();
754         }
756         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
758         if ($recursive) {
760             $dirs = $includedirs ? "" : "AND filename <> '.'";
761             $length = core_text::strlen($filepath);
763             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
764                       FROM {files} f
765                  LEFT JOIN {files_reference} r
766                            ON f.referencefileid = r.id
767                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
768                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
769                            AND f.id <> :dirid
770                            $dirs
771                            $orderby";
772             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
774             $files = array();
775             $dirs  = array();
776             $filerecords = $DB->get_records_sql($sql, $params);
777             foreach ($filerecords as $filerecord) {
778                 if ($filerecord->filename == '.') {
779                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
780                 } else {
781                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
782                 }
783             }
784             $result = array_merge($dirs, $files);
786         } else {
787             $result = array();
788             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
790             $length = core_text::strlen($filepath);
792             if ($includedirs) {
793                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
794                           FROM {files} f
795                      LEFT JOIN {files_reference} r
796                                ON f.referencefileid = r.id
797                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
798                                AND f.itemid = :itemid AND f.filename = '.'
799                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
800                                AND f.id <> :dirid
801                                $orderby";
802                 $reqlevel = substr_count($filepath, '/') + 1;
803                 $filerecords = $DB->get_records_sql($sql, $params);
804                 foreach ($filerecords as $filerecord) {
805                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
806                         continue;
807                     }
808                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
809                 }
810             }
812             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
813                       FROM {files} f
814                  LEFT JOIN {files_reference} r
815                            ON f.referencefileid = r.id
816                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
817                            AND f.filepath = :filepath AND f.filename <> '.'
818                            $orderby";
820             $filerecords = $DB->get_records_sql($sql, $params);
821             foreach ($filerecords as $filerecord) {
822                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
823             }
824         }
826         return $result;
827     }
829     /**
830      * Delete all area files (optionally limited by itemid).
831      *
832      * @param int $contextid context ID
833      * @param string $component component
834      * @param string $filearea file area or all areas in context if not specified
835      * @param int $itemid item ID or all files if not specified
836      * @return bool success
837      */
838     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
839         global $DB;
841         $conditions = array('contextid'=>$contextid);
842         if ($component !== false) {
843             $conditions['component'] = $component;
844         }
845         if ($filearea !== false) {
846             $conditions['filearea'] = $filearea;
847         }
848         if ($itemid !== false) {
849             $conditions['itemid'] = $itemid;
850         }
852         $filerecords = $DB->get_records('files', $conditions);
853         foreach ($filerecords as $filerecord) {
854             $this->get_file_instance($filerecord)->delete();
855         }
857         return true; // BC only
858     }
860     /**
861      * Delete all the files from certain areas where itemid is limited by an
862      * arbitrary bit of SQL.
863      *
864      * @param int $contextid the id of the context the files belong to. Must be given.
865      * @param string $component the owning component. Must be given.
866      * @param string $filearea the file area name. Must be given.
867      * @param string $itemidstest an SQL fragment that the itemid must match. Used
868      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
869      *      and may not used named parameters called contextid, component or filearea.
870      * @param array $params any query params used by $itemidstest.
871      */
872     public function delete_area_files_select($contextid, $component,
873             $filearea, $itemidstest, array $params = null) {
874         global $DB;
876         $where = "contextid = :contextid
877                 AND component = :component
878                 AND filearea = :filearea
879                 AND itemid $itemidstest";
880         $params['contextid'] = $contextid;
881         $params['component'] = $component;
882         $params['filearea'] = $filearea;
884         $filerecords = $DB->get_recordset_select('files', $where, $params);
885         foreach ($filerecords as $filerecord) {
886             $this->get_file_instance($filerecord)->delete();
887         }
888         $filerecords->close();
889     }
891     /**
892      * Delete all files associated with the given component.
893      *
894      * @param string $component the component owning the file
895      */
896     public function delete_component_files($component) {
897         global $DB;
899         $filerecords = $DB->get_recordset('files', array('component' => $component));
900         foreach ($filerecords as $filerecord) {
901             $this->get_file_instance($filerecord)->delete();
902         }
903         $filerecords->close();
904     }
906     /**
907      * Move all the files in a file area from one context to another.
908      *
909      * @param int $oldcontextid the context the files are being moved from.
910      * @param int $newcontextid the context the files are being moved to.
911      * @param string $component the plugin that these files belong to.
912      * @param string $filearea the name of the file area.
913      * @param int $itemid file item ID
914      * @return int the number of files moved, for information.
915      */
916     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
917         // Note, this code is based on some code that Petr wrote in
918         // forum_move_attachments in mod/forum/lib.php. I moved it here because
919         // I needed it in the question code too.
920         $count = 0;
922         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
923         foreach ($oldfiles as $oldfile) {
924             $filerecord = new stdClass();
925             $filerecord->contextid = $newcontextid;
926             $this->create_file_from_storedfile($filerecord, $oldfile);
927             $count += 1;
928         }
930         if ($count) {
931             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
932         }
934         return $count;
935     }
937     /**
938      * Recursively creates directory.
939      *
940      * @param int $contextid context ID
941      * @param string $component component
942      * @param string $filearea file area
943      * @param int $itemid item ID
944      * @param string $filepath file path
945      * @param int $userid the user ID
946      * @return bool success
947      */
948     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
949         global $DB;
951         // validate all parameters, we do not want any rubbish stored in database, right?
952         if (!is_number($contextid) or $contextid < 1) {
953             throw new file_exception('storedfileproblem', 'Invalid contextid');
954         }
956         $component = clean_param($component, PARAM_COMPONENT);
957         if (empty($component)) {
958             throw new file_exception('storedfileproblem', 'Invalid component');
959         }
961         $filearea = clean_param($filearea, PARAM_AREA);
962         if (empty($filearea)) {
963             throw new file_exception('storedfileproblem', 'Invalid filearea');
964         }
966         if (!is_number($itemid) or $itemid < 0) {
967             throw new file_exception('storedfileproblem', 'Invalid itemid');
968         }
970         $filepath = clean_param($filepath, PARAM_PATH);
971         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
972             // path must start and end with '/'
973             throw new file_exception('storedfileproblem', 'Invalid file path');
974         }
976         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
978         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
979             return $dir_info;
980         }
982         static $contenthash = null;
983         if (!$contenthash) {
984             $this->add_string_to_pool('');
985             $contenthash = self::hash_from_string('');
986         }
988         $now = time();
990         $dir_record = new stdClass();
991         $dir_record->contextid = $contextid;
992         $dir_record->component = $component;
993         $dir_record->filearea  = $filearea;
994         $dir_record->itemid    = $itemid;
995         $dir_record->filepath  = $filepath;
996         $dir_record->filename  = '.';
997         $dir_record->contenthash  = $contenthash;
998         $dir_record->filesize  = 0;
1000         $dir_record->timecreated  = $now;
1001         $dir_record->timemodified = $now;
1002         $dir_record->mimetype     = null;
1003         $dir_record->userid       = $userid;
1005         $dir_record->pathnamehash = $pathnamehash;
1007         $DB->insert_record('files', $dir_record);
1008         $dir_info = $this->get_file_by_hash($pathnamehash);
1010         if ($filepath !== '/') {
1011             //recurse to parent dirs
1012             $filepath = trim($filepath, '/');
1013             $filepath = explode('/', $filepath);
1014             array_pop($filepath);
1015             $filepath = implode('/', $filepath);
1016             $filepath = ($filepath === '') ? '/' : "/$filepath/";
1017             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1018         }
1020         return $dir_info;
1021     }
1023     /**
1024      * Add new file record to database and handle callbacks.
1025      *
1026      * @param stdClass $newrecord
1027      */
1028     protected function create_file($newrecord) {
1029         global $DB;
1030         $newrecord->id = $DB->insert_record('files', $newrecord);
1032         if ($newrecord->filename !== '.') {
1033             // Callback for file created.
1034             if ($pluginsfunction = get_plugins_with_function('after_file_created')) {
1035                 foreach ($pluginsfunction as $plugintype => $plugins) {
1036                     foreach ($plugins as $pluginfunction) {
1037                         $pluginfunction($newrecord);
1038                     }
1039                 }
1040             }
1041         }
1042     }
1044     /**
1045      * Add new local file based on existing local file.
1046      *
1047      * @param stdClass|array $filerecord object or array describing changes
1048      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1049      * @return stored_file instance of newly created file
1050      */
1051     public function create_file_from_storedfile($filerecord, $fileorid) {
1052         global $DB;
1054         if ($fileorid instanceof stored_file) {
1055             $fid = $fileorid->get_id();
1056         } else {
1057             $fid = $fileorid;
1058         }
1060         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1062         unset($filerecord['id']);
1063         unset($filerecord['filesize']);
1064         unset($filerecord['contenthash']);
1065         unset($filerecord['pathnamehash']);
1067         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1068                   FROM {files} f
1069              LEFT JOIN {files_reference} r
1070                        ON f.referencefileid = r.id
1071                  WHERE f.id = ?";
1073         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1074             throw new file_exception('storedfileproblem', 'File does not exist');
1075         }
1077         unset($newrecord->id);
1079         foreach ($filerecord as $key => $value) {
1080             // validate all parameters, we do not want any rubbish stored in database, right?
1081             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1082                 throw new file_exception('storedfileproblem', 'Invalid contextid');
1083             }
1085             if ($key == 'component') {
1086                 $value = clean_param($value, PARAM_COMPONENT);
1087                 if (empty($value)) {
1088                     throw new file_exception('storedfileproblem', 'Invalid component');
1089                 }
1090             }
1092             if ($key == 'filearea') {
1093                 $value = clean_param($value, PARAM_AREA);
1094                 if (empty($value)) {
1095                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1096                 }
1097             }
1099             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1100                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1101             }
1104             if ($key == 'filepath') {
1105                 $value = clean_param($value, PARAM_PATH);
1106                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1107                     // path must start and end with '/'
1108                     throw new file_exception('storedfileproblem', 'Invalid file path');
1109                 }
1110             }
1112             if ($key == 'filename') {
1113                 $value = clean_param($value, PARAM_FILE);
1114                 if ($value === '') {
1115                     // path must start and end with '/'
1116                     throw new file_exception('storedfileproblem', 'Invalid file name');
1117                 }
1118             }
1120             if ($key === 'timecreated' or $key === 'timemodified') {
1121                 if (!is_number($value)) {
1122                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1123                 }
1124                 if ($value < 0) {
1125                     //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)
1126                     $value = 0;
1127                 }
1128             }
1130             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1131                 $value = clean_param($value, PARAM_INT);
1132             }
1134             $newrecord->$key = $value;
1135         }
1137         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1139         if ($newrecord->filename === '.') {
1140             // special case - only this function supports directories ;-)
1141             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1142             // update the existing directory with the new data
1143             $newrecord->id = $directory->get_id();
1144             $DB->update_record('files', $newrecord);
1145             return $this->get_file_instance($newrecord);
1146         }
1148         // note: referencefileid is copied from the original file so that
1149         // creating a new file from an existing alias creates new alias implicitly.
1150         // here we just check the database consistency.
1151         if (!empty($newrecord->repositoryid)) {
1152             // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files
1153             // where saved from a draft area.
1154             $newrecord->referencefileid = $this->get_or_create_referencefileid($newrecord->repositoryid, $newrecord->reference);
1155         }
1157         try {
1158             $this->create_file($newrecord);
1159         } catch (dml_exception $e) {
1160             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1161                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1162         }
1165         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1167         return $this->get_file_instance($newrecord);
1168     }
1170     /**
1171      * Add new local file.
1172      *
1173      * @param stdClass|array $filerecord object or array describing file
1174      * @param string $url the URL to the file
1175      * @param array $options {@link download_file_content()} options
1176      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1177      * @return stored_file
1178      */
1179     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1181         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1182         $filerecord = (object)$filerecord; // We support arrays too.
1184         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1185         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1186         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1187         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1188         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1189         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1190         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1192         if (!isset($filerecord->filename)) {
1193             $parts = explode('/', $url);
1194             $filename = array_pop($parts);
1195             $filerecord->filename = clean_param($filename, PARAM_FILE);
1196         }
1197         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1198         $filerecord->source = clean_param($source, PARAM_URL);
1200         if ($usetempfile) {
1201             check_dir_exists($this->tempdir);
1202             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1203             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1204             if ($content === false) {
1205                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1206             }
1207             try {
1208                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1209                 @unlink($tmpfile);
1210                 return $newfile;
1211             } catch (Exception $e) {
1212                 @unlink($tmpfile);
1213                 throw $e;
1214             }
1216         } else {
1217             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1218             if ($content === false) {
1219                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1220             }
1221             return $this->create_file_from_string($filerecord, $content);
1222         }
1223     }
1225     /**
1226      * Add new local file.
1227      *
1228      * @param stdClass|array $filerecord object or array describing file
1229      * @param string $pathname path to file or content of file
1230      * @return stored_file
1231      */
1232     public function create_file_from_pathname($filerecord, $pathname) {
1233         global $DB;
1235         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1236         $filerecord = (object)$filerecord; // We support arrays too.
1238         // validate all parameters, we do not want any rubbish stored in database, right?
1239         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1240             throw new file_exception('storedfileproblem', 'Invalid contextid');
1241         }
1243         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1244         if (empty($filerecord->component)) {
1245             throw new file_exception('storedfileproblem', 'Invalid component');
1246         }
1248         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1249         if (empty($filerecord->filearea)) {
1250             throw new file_exception('storedfileproblem', 'Invalid filearea');
1251         }
1253         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1254             throw new file_exception('storedfileproblem', 'Invalid itemid');
1255         }
1257         if (!empty($filerecord->sortorder)) {
1258             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1259                 $filerecord->sortorder = 0;
1260             }
1261         } else {
1262             $filerecord->sortorder = 0;
1263         }
1265         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1266         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1267             // path must start and end with '/'
1268             throw new file_exception('storedfileproblem', 'Invalid file path');
1269         }
1271         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1272         if ($filerecord->filename === '') {
1273             // filename must not be empty
1274             throw new file_exception('storedfileproblem', 'Invalid file name');
1275         }
1277         $now = time();
1278         if (isset($filerecord->timecreated)) {
1279             if (!is_number($filerecord->timecreated)) {
1280                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1281             }
1282             if ($filerecord->timecreated < 0) {
1283                 //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)
1284                 $filerecord->timecreated = 0;
1285             }
1286         } else {
1287             $filerecord->timecreated = $now;
1288         }
1290         if (isset($filerecord->timemodified)) {
1291             if (!is_number($filerecord->timemodified)) {
1292                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1293             }
1294             if ($filerecord->timemodified < 0) {
1295                 //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)
1296                 $filerecord->timemodified = 0;
1297             }
1298         } else {
1299             $filerecord->timemodified = $now;
1300         }
1302         $newrecord = new stdClass();
1304         $newrecord->contextid = $filerecord->contextid;
1305         $newrecord->component = $filerecord->component;
1306         $newrecord->filearea  = $filerecord->filearea;
1307         $newrecord->itemid    = $filerecord->itemid;
1308         $newrecord->filepath  = $filerecord->filepath;
1309         $newrecord->filename  = $filerecord->filename;
1311         $newrecord->timecreated  = $filerecord->timecreated;
1312         $newrecord->timemodified = $filerecord->timemodified;
1313         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1314         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1315         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1316         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1317         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1318         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1319         $newrecord->sortorder    = $filerecord->sortorder;
1321         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname, null, $newrecord);
1323         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1325         try {
1326             $this->create_file($newrecord);
1327         } catch (dml_exception $e) {
1328             if ($newfile) {
1329                 $this->filesystem->remove_file($newrecord->contenthash);
1330             }
1331             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1332                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1333         }
1335         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1337         return $this->get_file_instance($newrecord);
1338     }
1340     /**
1341      * Add new local file.
1342      *
1343      * @param stdClass|array $filerecord object or array describing file
1344      * @param string $content content of file
1345      * @return stored_file
1346      */
1347     public function create_file_from_string($filerecord, $content) {
1348         global $DB;
1350         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1351         $filerecord = (object)$filerecord; // We support arrays too.
1353         // validate all parameters, we do not want any rubbish stored in database, right?
1354         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1355             throw new file_exception('storedfileproblem', 'Invalid contextid');
1356         }
1358         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1359         if (empty($filerecord->component)) {
1360             throw new file_exception('storedfileproblem', 'Invalid component');
1361         }
1363         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1364         if (empty($filerecord->filearea)) {
1365             throw new file_exception('storedfileproblem', 'Invalid filearea');
1366         }
1368         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1369             throw new file_exception('storedfileproblem', 'Invalid itemid');
1370         }
1372         if (!empty($filerecord->sortorder)) {
1373             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1374                 $filerecord->sortorder = 0;
1375             }
1376         } else {
1377             $filerecord->sortorder = 0;
1378         }
1380         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1381         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1382             // path must start and end with '/'
1383             throw new file_exception('storedfileproblem', 'Invalid file path');
1384         }
1386         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1387         if ($filerecord->filename === '') {
1388             // path must start and end with '/'
1389             throw new file_exception('storedfileproblem', 'Invalid file name');
1390         }
1392         $now = time();
1393         if (isset($filerecord->timecreated)) {
1394             if (!is_number($filerecord->timecreated)) {
1395                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1396             }
1397             if ($filerecord->timecreated < 0) {
1398                 //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)
1399                 $filerecord->timecreated = 0;
1400             }
1401         } else {
1402             $filerecord->timecreated = $now;
1403         }
1405         if (isset($filerecord->timemodified)) {
1406             if (!is_number($filerecord->timemodified)) {
1407                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1408             }
1409             if ($filerecord->timemodified < 0) {
1410                 //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)
1411                 $filerecord->timemodified = 0;
1412             }
1413         } else {
1414             $filerecord->timemodified = $now;
1415         }
1417         $newrecord = new stdClass();
1419         $newrecord->contextid = $filerecord->contextid;
1420         $newrecord->component = $filerecord->component;
1421         $newrecord->filearea  = $filerecord->filearea;
1422         $newrecord->itemid    = $filerecord->itemid;
1423         $newrecord->filepath  = $filerecord->filepath;
1424         $newrecord->filename  = $filerecord->filename;
1426         $newrecord->timecreated  = $filerecord->timecreated;
1427         $newrecord->timemodified = $filerecord->timemodified;
1428         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1429         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1430         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1431         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1432         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1433         $newrecord->sortorder    = $filerecord->sortorder;
1435         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content, $newrecord);
1436         if (empty($filerecord->mimetype)) {
1437             $newrecord->mimetype = $this->filesystem->mimetype_from_hash($newrecord->contenthash, $newrecord->filename);
1438         } else {
1439             $newrecord->mimetype = $filerecord->mimetype;
1440         }
1442         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1444         try {
1445             $this->create_file($newrecord);
1446         } catch (dml_exception $e) {
1447             if ($newfile) {
1448                 $this->filesystem->remove_file($newrecord->contenthash);
1449             }
1450             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1451                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1452         }
1454         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1456         return $this->get_file_instance($newrecord);
1457     }
1459     /**
1460      * Synchronise stored file from file.
1461      *
1462      * @param stored_file $file Stored file to synchronise.
1463      * @param string $path Path to the file to synchronise from.
1464      * @param stdClass $filerecord The file record from the database.
1465      */
1466     public function synchronise_stored_file_from_file(stored_file $file, $path, $filerecord) {
1467         list($contenthash, $filesize) = $this->add_file_to_pool($path, null, $filerecord);
1468         $file->set_synchronized($contenthash, $filesize);
1469     }
1471     /**
1472      * Synchronise stored file from string.
1473      *
1474      * @param stored_file $file Stored file to synchronise.
1475      * @param string $content File content.
1476      * @param stdClass $filerecord The file record from the database.
1477      */
1478     public function synchronise_stored_file_from_string(stored_file $file, $content, $filerecord) {
1479         list($contenthash, $filesize) = $this->add_string_to_pool($content, $filerecord);
1480         $file->set_synchronized($contenthash, $filesize);
1481     }
1483     /**
1484      * Create a new alias/shortcut file from file reference information
1485      *
1486      * @param stdClass|array $filerecord object or array describing the new file
1487      * @param int $repositoryid the id of the repository that provides the original file
1488      * @param string $reference the information required by the repository to locate the original file
1489      * @param array $options options for creating the new file
1490      * @return stored_file
1491      */
1492     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1493         global $DB;
1495         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1496         $filerecord = (object)$filerecord; // We support arrays too.
1498         // validate all parameters, we do not want any rubbish stored in database, right?
1499         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1500             throw new file_exception('storedfileproblem', 'Invalid contextid');
1501         }
1503         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1504         if (empty($filerecord->component)) {
1505             throw new file_exception('storedfileproblem', 'Invalid component');
1506         }
1508         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1509         if (empty($filerecord->filearea)) {
1510             throw new file_exception('storedfileproblem', 'Invalid filearea');
1511         }
1513         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1514             throw new file_exception('storedfileproblem', 'Invalid itemid');
1515         }
1517         if (!empty($filerecord->sortorder)) {
1518             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1519                 $filerecord->sortorder = 0;
1520             }
1521         } else {
1522             $filerecord->sortorder = 0;
1523         }
1525         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1526         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1527         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1528         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1529         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1530         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1531         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1532         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1533             // Path must start and end with '/'.
1534             throw new file_exception('storedfileproblem', 'Invalid file path');
1535         }
1537         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1538         if ($filerecord->filename === '') {
1539             // Path must start and end with '/'.
1540             throw new file_exception('storedfileproblem', 'Invalid file name');
1541         }
1543         $now = time();
1544         if (isset($filerecord->timecreated)) {
1545             if (!is_number($filerecord->timecreated)) {
1546                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1547             }
1548             if ($filerecord->timecreated < 0) {
1549                 // 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)
1550                 $filerecord->timecreated = 0;
1551             }
1552         } else {
1553             $filerecord->timecreated = $now;
1554         }
1556         if (isset($filerecord->timemodified)) {
1557             if (!is_number($filerecord->timemodified)) {
1558                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1559             }
1560             if ($filerecord->timemodified < 0) {
1561                 // 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)
1562                 $filerecord->timemodified = 0;
1563             }
1564         } else {
1565             $filerecord->timemodified = $now;
1566         }
1568         $transaction = $DB->start_delegated_transaction();
1570         try {
1571             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1572         } catch (Exception $e) {
1573             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1574         }
1576         $existingfile = null;
1577         if (isset($filerecord->contenthash)) {
1578             $existingfile = $DB->get_record('files', array('contenthash' => $filerecord->contenthash));
1579         }
1580         if (!empty($existingfile)) {
1581             // There is an existing file already available.
1582             if (empty($filerecord->filesize)) {
1583                 $filerecord->filesize = $existingfile->filesize;
1584             } else {
1585                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1586             }
1587         } else {
1588             // Attempt to get the result of last synchronisation for this reference.
1589             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1590                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1591             if ($lastcontent) {
1592                 $filerecord->contenthash = $lastcontent->contenthash;
1593                 $filerecord->filesize = $lastcontent->filesize;
1594             } else {
1595                 // External file doesn't have content in moodle.
1596                 // So we create an empty file for it.
1597                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null, $filerecord);
1598             }
1599         }
1601         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1603         try {
1604             $filerecord->id = $DB->insert_record('files', $filerecord);
1605         } catch (dml_exception $e) {
1606             if (!empty($newfile)) {
1607                 $this->filesystem->remove_file($filerecord->contenthash);
1608             }
1609             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1610                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1611         }
1613         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1615         $transaction->allow_commit();
1617         // this will retrieve all reference information from DB as well
1618         return $this->get_file_by_id($filerecord->id);
1619     }
1621     /**
1622      * Creates new image file from existing.
1623      *
1624      * @param stdClass|array $filerecord object or array describing new file
1625      * @param int|stored_file $fid file id or stored file object
1626      * @param int $newwidth in pixels
1627      * @param int $newheight in pixels
1628      * @param bool $keepaspectratio whether or not keep aspect ratio
1629      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1630      * @return stored_file
1631      */
1632     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1633         if (!function_exists('imagecreatefromstring')) {
1634             //Most likely the GD php extension isn't installed
1635             //image conversion cannot succeed
1636             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1637         }
1639         if ($fid instanceof stored_file) {
1640             $fid = $fid->get_id();
1641         }
1643         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1645         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1646             throw new file_exception('storedfileproblem', 'File does not exist');
1647         }
1649         if (!$imageinfo = $file->get_imageinfo()) {
1650             throw new file_exception('storedfileproblem', 'File is not an image');
1651         }
1653         if (!isset($filerecord['filename'])) {
1654             $filerecord['filename'] = $file->get_filename();
1655         }
1657         if (!isset($filerecord['mimetype'])) {
1658             $filerecord['mimetype'] = $imageinfo['mimetype'];
1659         }
1661         $width    = $imageinfo['width'];
1662         $height   = $imageinfo['height'];
1664         if ($keepaspectratio) {
1665             if (0 >= $newwidth and 0 >= $newheight) {
1666                 // no sizes specified
1667                 $newwidth  = $width;
1668                 $newheight = $height;
1670             } else if (0 < $newwidth and 0 < $newheight) {
1671                 $xheight = ($newwidth*($height/$width));
1672                 if ($xheight < $newheight) {
1673                     $newheight = (int)$xheight;
1674                 } else {
1675                     $newwidth = (int)($newheight*($width/$height));
1676                 }
1678             } else if (0 < $newwidth) {
1679                 $newheight = (int)($newwidth*($height/$width));
1681             } else { //0 < $newheight
1682                 $newwidth = (int)($newheight*($width/$height));
1683             }
1685         } else {
1686             if (0 >= $newwidth) {
1687                 $newwidth = $width;
1688             }
1689             if (0 >= $newheight) {
1690                 $newheight = $height;
1691             }
1692         }
1694         // The original image.
1695         $img = imagecreatefromstring($file->get_content());
1697         // A new true color image where we will copy our original image.
1698         $newimg = imagecreatetruecolor($newwidth, $newheight);
1700         // Determine if the file supports transparency.
1701         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1703         // Maintain transparency.
1704         if ($hasalpha) {
1705             imagealphablending($newimg, true);
1707             // Get the current transparent index for the original image.
1708             $colour = imagecolortransparent($img);
1709             if ($colour == -1) {
1710                 // Set a transparent colour index if there's none.
1711                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1712                 // Save full alpha channel.
1713                 imagesavealpha($newimg, true);
1714             }
1715             imagecolortransparent($newimg, $colour);
1716             imagefill($newimg, 0, 0, $colour);
1717         }
1719         // Process the image to be output.
1720         if ($height != $newheight or $width != $newwidth) {
1721             // Resample if the dimensions differ from the original.
1722             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1723                 // weird
1724                 throw new file_exception('storedfileproblem', 'Can not resize image');
1725             }
1726             imagedestroy($img);
1727             $img = $newimg;
1729         } else if ($hasalpha) {
1730             // Just copy to the new image with the alpha channel.
1731             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1732                 // Weird.
1733                 throw new file_exception('storedfileproblem', 'Can not copy image');
1734             }
1735             imagedestroy($img);
1736             $img = $newimg;
1738         } else {
1739             // No particular processing needed for the original image.
1740             imagedestroy($newimg);
1741         }
1743         ob_start();
1744         switch ($filerecord['mimetype']) {
1745             case 'image/gif':
1746                 imagegif($img);
1747                 break;
1749             case 'image/jpeg':
1750                 if (is_null($quality)) {
1751                     imagejpeg($img);
1752                 } else {
1753                     imagejpeg($img, NULL, $quality);
1754                 }
1755                 break;
1757             case 'image/png':
1758                 $quality = (int)$quality;
1760                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1761                 // the latter of which can go to 100, we need to make sure that quality here is
1762                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1763                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1764                 imagepng($img, NULL, $quality, NULL);
1765                 break;
1767             default:
1768                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1769         }
1771         $content = ob_get_contents();
1772         ob_end_clean();
1773         imagedestroy($img);
1775         if (!$content) {
1776             throw new file_exception('storedfileproblem', 'Can not convert image');
1777         }
1779         return $this->create_file_from_string($filerecord, $content);
1780     }
1782     /**
1783      * Add file content to sha1 pool.
1784      *
1785      * @param string $pathname path to file
1786      * @param string|null $contenthash sha1 hash of content if known (performance only)
1787      * @param stdClass|null $newrecord New file record
1788      * @return array (contenthash, filesize, newfile)
1789      */
1790     public function add_file_to_pool($pathname, $contenthash = null, $newrecord = null) {
1791         $this->call_before_file_created_plugin_functions($newrecord, $pathname);
1792         return $this->filesystem->add_file_from_path($pathname, $contenthash);
1793     }
1795     /**
1796      * Add string content to sha1 pool.
1797      *
1798      * @param string $content file content - binary string
1799      * @return array (contenthash, filesize, newfile)
1800      */
1801     public function add_string_to_pool($content, $newrecord = null) {
1802         $this->call_before_file_created_plugin_functions($newrecord, null, $content);
1803         return $this->filesystem->add_file_from_string($content);
1804     }
1806     /**
1807      * before_file_created hook.
1808      *
1809      * @param stdClass|null $newrecord New file record.
1810      * @param string|null $pathname Path to file.
1811      * @param string|null $content File content.
1812      */
1813     protected function call_before_file_created_plugin_functions($newrecord, $pathname = null, $content = null) {
1814         $pluginsfunction = get_plugins_with_function('before_file_created');
1815         foreach ($pluginsfunction as $plugintype => $plugins) {
1816             foreach ($plugins as $pluginfunction) {
1817                 $pluginfunction($newrecord, ['pathname' => $pathname, 'content' => $content]);
1818             }
1819         }
1820     }
1822     /**
1823      * Serve file content using X-Sendfile header.
1824      * Please make sure that all headers are already sent
1825      * and the all access control checks passed.
1826      *
1827      * @param string $contenthash sah1 hash of the file content to be served
1828      * @return bool success
1829      */
1830     public function xsendfile($contenthash) {
1831         return $this->filesystem->xsendfile($contenthash);
1832     }
1834     /**
1835      * Content exists
1836      *
1837      * @param string $contenthash
1838      * @return bool
1839      * @deprecated since 3.3
1840      */
1841     public function content_exists($contenthash) {
1842         debugging('The content_exists function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1844         return false;
1845     }
1847     /**
1848      * Tries to recover missing content of file from trash.
1849      *
1850      * @param stored_file $file stored_file instance
1851      * @return bool success
1852      * @deprecated since 3.3
1853      */
1854     public function try_content_recovery($file) {
1855         debugging('The try_content_recovery function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1857         return false;
1858     }
1860     /**
1861      * When user referring to a moodle file, we build the reference field
1862      *
1863      * @param array $params
1864      * @return string
1865      */
1866     public static function pack_reference($params) {
1867         $params = (array)$params;
1868         $reference = array();
1869         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1870         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1871         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1872         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1873         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1874         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1875         return base64_encode(serialize($reference));
1876     }
1878     /**
1879      * Unpack reference field
1880      *
1881      * @param string $str
1882      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1883      * @throws file_reference_exception if the $str does not have the expected format
1884      * @return array
1885      */
1886     public static function unpack_reference($str, $cleanparams = false) {
1887         $decoded = base64_decode($str, true);
1888         if ($decoded === false) {
1889             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1890         }
1891         $params = @unserialize($decoded); // hide E_NOTICE
1892         if ($params === false) {
1893             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1894         }
1895         if (is_array($params) && $cleanparams) {
1896             $params = array(
1897                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1898                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1899                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1900                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1901                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1902                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1903             );
1904         }
1905         return $params;
1906     }
1908     /**
1909      * Search through the server files.
1910      *
1911      * The query parameter will be used in conjuction with the SQL directive
1912      * LIKE, so include '%' in it if you need to. This search will always ignore
1913      * user files and directories. Note that the search is case insensitive.
1914      *
1915      * This query can quickly become inefficient so use it sparignly.
1916      *
1917      * @param  string  $query The string used with SQL LIKE.
1918      * @param  integer $from  The offset to start the search at.
1919      * @param  integer $limit The maximum number of results.
1920      * @param  boolean $count When true this methods returns the number of results availabe,
1921      *                        disregarding the parameters $from and $limit.
1922      * @return int|array      Integer when count, otherwise array of stored_file objects.
1923      */
1924     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
1925         global $DB;
1926         $params = array(
1927             'contextlevel' => CONTEXT_USER,
1928             'directory' => '.',
1929             'query' => $query
1930         );
1932         if ($count) {
1933             $select = 'COUNT(1)';
1934         } else {
1935             $select = self::instance_sql_fields('f', 'r');
1936         }
1937         $like = $DB->sql_like('f.filename', ':query', false);
1939         $sql = "SELECT $select
1940                   FROM {files} f
1941              LEFT JOIN {files_reference} r
1942                     ON f.referencefileid = r.id
1943                   JOIN {context} c
1944                     ON f.contextid = c.id
1945                  WHERE c.contextlevel <> :contextlevel
1946                    AND f.filename <> :directory
1947                    AND " . $like . "";
1949         if ($count) {
1950             return $DB->count_records_sql($sql, $params);
1951         }
1953         $sql .= " ORDER BY f.filename";
1955         $result = array();
1956         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
1957         foreach ($filerecords as $filerecord) {
1958             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1959         }
1960         $filerecords->close();
1962         return $result;
1963     }
1965     /**
1966      * Returns all aliases that refer to some stored_file via the given reference
1967      *
1968      * All repositories that provide access to a stored_file are expected to use
1969      * {@link self::pack_reference()}. This method can't be used if the given reference
1970      * does not use this format or if you are looking for references to an external file
1971      * (for example it can't be used to search for all aliases that refer to a given
1972      * Dropbox or Box.net file).
1973      *
1974      * Aliases in user draft areas are excluded from the returned list.
1975      *
1976      * @param string $reference identification of the referenced file
1977      * @return array of stored_file indexed by its pathnamehash
1978      */
1979     public function search_references($reference) {
1980         global $DB;
1982         if (is_null($reference)) {
1983             throw new coding_exception('NULL is not a valid reference to an external file');
1984         }
1986         // Give {@link self::unpack_reference()} a chance to throw exception if the
1987         // reference is not in a valid format.
1988         self::unpack_reference($reference);
1990         $referencehash = sha1($reference);
1992         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1993                   FROM {files} f
1994                   JOIN {files_reference} r ON f.referencefileid = r.id
1995                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1996                  WHERE r.referencehash = ?
1997                        AND (f.component <> ? OR f.filearea <> ?)";
1999         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2000         $files = array();
2001         foreach ($rs as $filerecord) {
2002             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2003         }
2005         return $files;
2006     }
2008     /**
2009      * Returns the number of aliases that refer to some stored_file via the given reference
2010      *
2011      * All repositories that provide access to a stored_file are expected to use
2012      * {@link self::pack_reference()}. This method can't be used if the given reference
2013      * does not use this format or if you are looking for references to an external file
2014      * (for example it can't be used to count aliases that refer to a given Dropbox or
2015      * Box.net file).
2016      *
2017      * Aliases in user draft areas are not counted.
2018      *
2019      * @param string $reference identification of the referenced file
2020      * @return int
2021      */
2022     public function search_references_count($reference) {
2023         global $DB;
2025         if (is_null($reference)) {
2026             throw new coding_exception('NULL is not a valid reference to an external file');
2027         }
2029         // Give {@link self::unpack_reference()} a chance to throw exception if the
2030         // reference is not in a valid format.
2031         self::unpack_reference($reference);
2033         $referencehash = sha1($reference);
2035         $sql = "SELECT COUNT(f.id)
2036                   FROM {files} f
2037                   JOIN {files_reference} r ON f.referencefileid = r.id
2038                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2039                  WHERE r.referencehash = ?
2040                        AND (f.component <> ? OR f.filearea <> ?)";
2042         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2043     }
2045     /**
2046      * Returns all aliases that link to the given stored_file
2047      *
2048      * Aliases in user draft areas are excluded from the returned list.
2049      *
2050      * @param stored_file $storedfile
2051      * @return array of stored_file
2052      */
2053     public function get_references_by_storedfile(stored_file $storedfile) {
2054         global $DB;
2056         $params = array();
2057         $params['contextid'] = $storedfile->get_contextid();
2058         $params['component'] = $storedfile->get_component();
2059         $params['filearea']  = $storedfile->get_filearea();
2060         $params['itemid']    = $storedfile->get_itemid();
2061         $params['filename']  = $storedfile->get_filename();
2062         $params['filepath']  = $storedfile->get_filepath();
2064         return $this->search_references(self::pack_reference($params));
2065     }
2067     /**
2068      * Returns the number of aliases that link to the given stored_file
2069      *
2070      * Aliases in user draft areas are not counted.
2071      *
2072      * @param stored_file $storedfile
2073      * @return int
2074      */
2075     public function get_references_count_by_storedfile(stored_file $storedfile) {
2076         global $DB;
2078         $params = array();
2079         $params['contextid'] = $storedfile->get_contextid();
2080         $params['component'] = $storedfile->get_component();
2081         $params['filearea']  = $storedfile->get_filearea();
2082         $params['itemid']    = $storedfile->get_itemid();
2083         $params['filename']  = $storedfile->get_filename();
2084         $params['filepath']  = $storedfile->get_filepath();
2086         return $this->search_references_count(self::pack_reference($params));
2087     }
2089     /**
2090      * Updates all files that are referencing this file with the new contenthash
2091      * and filesize
2092      *
2093      * @param stored_file $storedfile
2094      */
2095     public function update_references_to_storedfile(stored_file $storedfile) {
2096         global $CFG, $DB;
2097         $params = array();
2098         $params['contextid'] = $storedfile->get_contextid();
2099         $params['component'] = $storedfile->get_component();
2100         $params['filearea']  = $storedfile->get_filearea();
2101         $params['itemid']    = $storedfile->get_itemid();
2102         $params['filename']  = $storedfile->get_filename();
2103         $params['filepath']  = $storedfile->get_filepath();
2104         $reference = self::pack_reference($params);
2105         $referencehash = sha1($reference);
2107         $sql = "SELECT repositoryid, id FROM {files_reference}
2108                  WHERE referencehash = ?";
2109         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2111         $now = time();
2112         foreach ($rs as $record) {
2113             $this->update_references($record->id, $now, null,
2114                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2115         }
2116         $rs->close();
2117     }
2119     /**
2120      * Convert file alias to local file
2121      *
2122      * @throws moodle_exception if file could not be downloaded
2123      *
2124      * @param stored_file $storedfile a stored_file instances
2125      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2126      * @return stored_file stored_file
2127      */
2128     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2129         global $CFG;
2130         $storedfile->import_external_file_contents($maxbytes);
2131         $storedfile->delete_reference();
2132         return $storedfile;
2133     }
2135     /**
2136      * Return mimetype by given file pathname.
2137      *
2138      * If file has a known extension, we return the mimetype based on extension.
2139      * Otherwise (when possible) we try to get the mimetype from file contents.
2140      *
2141      * @param string $fullpath Full path to the file on disk
2142      * @param string $filename Correct file name with extension, if omitted will be taken from $path
2143      * @return string
2144      */
2145     public static function mimetype($fullpath, $filename = null) {
2146         if (empty($filename)) {
2147             $filename = $fullpath;
2148         }
2150         // The mimeinfo function determines the mimetype purely based on the file extension.
2151         $type = mimeinfo('type', $filename);
2153         if ($type === 'document/unknown') {
2154             // The type is unknown. Inspect the file now.
2155             $type = self::mimetype_from_file($fullpath);
2156         }
2157         return $type;
2158     }
2160     /**
2161      * Inspect a file on disk for it's mimetype.
2162      *
2163      * @param string $fullpath Path to file on disk
2164      * @return string The mimetype
2165      */
2166     public static function mimetype_from_file($fullpath) {
2167         if (file_exists($fullpath)) {
2168             // The type is unknown. Attempt to look up the file type now.
2169             $finfo = new finfo(FILEINFO_MIME_TYPE);
2170             return mimeinfo_from_type('type', $finfo->file($fullpath));
2171         }
2173         return 'document/unknown';
2174     }
2176     /**
2177      * Cron cleanup job.
2178      */
2179     public function cron() {
2180         global $CFG, $DB;
2181         require_once($CFG->libdir.'/cronlib.php');
2183         // find out all stale draft areas (older than 4 days) and purge them
2184         // those are identified by time stamp of the /. root dir
2185         mtrace('Deleting old draft files... ', '');
2186         cron_trace_time_and_memory();
2187         $old = time() - 60*60*24*4;
2188         $sql = "SELECT *
2189                   FROM {files}
2190                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2191                        AND timecreated < :old";
2192         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2193         foreach ($rs as $dir) {
2194             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2195         }
2196         $rs->close();
2197         mtrace('done.');
2199         // remove orphaned preview files (that is files in the core preview filearea without
2200         // the existing original file)
2201         mtrace('Deleting orphaned preview files... ', '');
2202         cron_trace_time_and_memory();
2203         $sql = "SELECT p.*
2204                   FROM {files} p
2205              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2206                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2207                        AND o.id IS NULL";
2208         $syscontext = context_system::instance();
2209         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2210         foreach ($rs as $orphan) {
2211             $file = $this->get_file_instance($orphan);
2212             if (!$file->is_directory()) {
2213                 $file->delete();
2214             }
2215         }
2216         $rs->close();
2217         mtrace('done.');
2219         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2220         // the existing original file).
2221         mtrace('Deleting orphaned document conversion files... ', '');
2222         cron_trace_time_and_memory();
2223         $sql = "SELECT p.*
2224                   FROM {files} p
2225              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2226                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
2227                        AND o.id IS NULL";
2228         $syscontext = context_system::instance();
2229         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2230         foreach ($rs as $orphan) {
2231             $file = $this->get_file_instance($orphan);
2232             if (!$file->is_directory()) {
2233                 $file->delete();
2234             }
2235         }
2236         $rs->close();
2237         mtrace('done.');
2239         // remove trash pool files once a day
2240         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2241         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2242             require_once($CFG->libdir.'/filelib.php');
2243             // Delete files that are associated with a context that no longer exists.
2244             mtrace('Cleaning up files from deleted contexts... ', '');
2245             cron_trace_time_and_memory();
2246             $sql = "SELECT DISTINCT f.contextid
2247                     FROM {files} f
2248                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2249                     WHERE c.id IS NULL";
2250             $rs = $DB->get_recordset_sql($sql);
2251             if ($rs->valid()) {
2252                 $fs = get_file_storage();
2253                 foreach ($rs as $ctx) {
2254                     $fs->delete_area_files($ctx->contextid);
2255                 }
2256             }
2257             $rs->close();
2258             mtrace('done.');
2260             mtrace('Call filesystem cron tasks.', '');
2261             cron_trace_time_and_memory();
2262             $this->filesystem->cron();
2263             mtrace('done.');
2264         }
2265     }
2267     /**
2268      * Get the sql formated fields for a file instance to be created from a
2269      * {files} and {files_refernece} join.
2270      *
2271      * @param string $filesprefix the table prefix for the {files} table
2272      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2273      * @return string the sql to go after a SELECT
2274      */
2275     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2276         // Note, these fieldnames MUST NOT overlap between the two tables,
2277         // else problems like MDL-33172 occur.
2278         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2279             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2280             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2282         $referencefields = array('repositoryid' => 'repositoryid',
2283             'reference' => 'reference',
2284             'lastsync' => 'referencelastsync');
2286         // id is specifically named to prevent overlaping between the two tables.
2287         $fields = array();
2288         $fields[] = $filesprefix.'.id AS id';
2289         foreach ($filefields as $field) {
2290             $fields[] = "{$filesprefix}.{$field}";
2291         }
2293         foreach ($referencefields as $field => $alias) {
2294             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2295         }
2297         return implode(', ', $fields);
2298     }
2300     /**
2301      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2302      *
2303      * If the record already exists, its id is returned. If there is no such record yet,
2304      * new one is created (using the lastsync provided, too) and its id is returned.
2305      *
2306      * @param int $repositoryid
2307      * @param string $reference
2308      * @param int $lastsync
2309      * @param int $lifetime argument not used any more
2310      * @return int
2311      */
2312     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2313         global $DB;
2315         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2317         if ($id !== false) {
2318             // bah, that was easy
2319             return $id;
2320         }
2322         // no such record yet, create one
2323         try {
2324             $id = $DB->insert_record('files_reference', array(
2325                 'repositoryid'  => $repositoryid,
2326                 'reference'     => $reference,
2327                 'referencehash' => sha1($reference),
2328                 'lastsync'      => $lastsync));
2329         } catch (dml_exception $e) {
2330             // if inserting the new record failed, chances are that the race condition has just
2331             // occured and the unique index did not allow to create the second record with the same
2332             // repositoryid + reference combo
2333             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2334         }
2336         return $id;
2337     }
2339     /**
2340      * Returns the id of the record in {files_reference} that matches the passed parameters
2341      *
2342      * Depending on the required strictness, false can be returned. The behaviour is consistent
2343      * with standard DML methods.
2344      *
2345      * @param int $repositoryid
2346      * @param string $reference
2347      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2348      * @return int|bool
2349      */
2350     private function get_referencefileid($repositoryid, $reference, $strictness) {
2351         global $DB;
2353         return $DB->get_field('files_reference', 'id',
2354             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2355     }
2357     /**
2358      * Updates a reference to the external resource and all files that use it
2359      *
2360      * This function is called after synchronisation of an external file and updates the
2361      * contenthash, filesize and status of all files that reference this external file
2362      * as well as time last synchronised.
2363      *
2364      * @param int $referencefileid
2365      * @param int $lastsync
2366      * @param int $lifetime argument not used any more, liefetime is returned by repository
2367      * @param string $contenthash
2368      * @param int $filesize
2369      * @param int $status 0 if ok or 666 if source is missing
2370      * @param int $timemodified last time modified of the source, if known
2371      */
2372     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2373         global $DB;
2374         $referencefileid = clean_param($referencefileid, PARAM_INT);
2375         $lastsync = clean_param($lastsync, PARAM_INT);
2376         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2377         $filesize = clean_param($filesize, PARAM_INT);
2378         $status = clean_param($status, PARAM_INT);
2379         $params = array('contenthash' => $contenthash,
2380                     'filesize' => $filesize,
2381                     'status' => $status,
2382                     'referencefileid' => $referencefileid,
2383                     'timemodified' => $timemodified);
2384         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2385             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2386             WHERE referencefileid = :referencefileid', $params);
2387         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2388         $DB->update_record('files_reference', (object)$data);
2389     }
2391     /**
2392      * Calculate and return the contenthash of the supplied file.
2393      *
2394      * @param   string $filepath The path to the file on disk
2395      * @return  string The file's content hash
2396      */
2397     public static function hash_from_path($filepath) {
2398         return sha1_file($filepath);
2399     }
2401     /**
2402      * Calculate and return the contenthash of the supplied content.
2403      *
2404      * @param   string $content The file content
2405      * @return  string The file's content hash
2406      */
2407     public static function hash_from_string($content) {
2408         return sha1($content);
2409     }