44aa02b1c58118d307f52c302bf4b1b5ad5efc7e
[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->move_to_trash($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->move_to_trash($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      * Create a new alias/shortcut file from file reference information
1461      *
1462      * @param stdClass|array $filerecord object or array describing the new file
1463      * @param int $repositoryid the id of the repository that provides the original file
1464      * @param string $reference the information required by the repository to locate the original file
1465      * @param array $options options for creating the new file
1466      * @return stored_file
1467      */
1468     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1469         global $DB;
1471         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1472         $filerecord = (object)$filerecord; // We support arrays too.
1474         // validate all parameters, we do not want any rubbish stored in database, right?
1475         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1476             throw new file_exception('storedfileproblem', 'Invalid contextid');
1477         }
1479         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1480         if (empty($filerecord->component)) {
1481             throw new file_exception('storedfileproblem', 'Invalid component');
1482         }
1484         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1485         if (empty($filerecord->filearea)) {
1486             throw new file_exception('storedfileproblem', 'Invalid filearea');
1487         }
1489         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1490             throw new file_exception('storedfileproblem', 'Invalid itemid');
1491         }
1493         if (!empty($filerecord->sortorder)) {
1494             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1495                 $filerecord->sortorder = 0;
1496             }
1497         } else {
1498             $filerecord->sortorder = 0;
1499         }
1501         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1502         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1503         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1504         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1505         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1506         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1507         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1508         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1509             // Path must start and end with '/'.
1510             throw new file_exception('storedfileproblem', 'Invalid file path');
1511         }
1513         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1514         if ($filerecord->filename === '') {
1515             // Path must start and end with '/'.
1516             throw new file_exception('storedfileproblem', 'Invalid file name');
1517         }
1519         $now = time();
1520         if (isset($filerecord->timecreated)) {
1521             if (!is_number($filerecord->timecreated)) {
1522                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1523             }
1524             if ($filerecord->timecreated < 0) {
1525                 // 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)
1526                 $filerecord->timecreated = 0;
1527             }
1528         } else {
1529             $filerecord->timecreated = $now;
1530         }
1532         if (isset($filerecord->timemodified)) {
1533             if (!is_number($filerecord->timemodified)) {
1534                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1535             }
1536             if ($filerecord->timemodified < 0) {
1537                 // 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)
1538                 $filerecord->timemodified = 0;
1539             }
1540         } else {
1541             $filerecord->timemodified = $now;
1542         }
1544         $transaction = $DB->start_delegated_transaction();
1546         try {
1547             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1548         } catch (Exception $e) {
1549             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1550         }
1552         $existingfile = null;
1553         if (isset($filerecord->contenthash)) {
1554             $existingfile = $DB->get_record('files', array('contenthash' => $filerecord->contenthash));
1555         }
1556         if (!empty($existingfile)) {
1557             // There is an existing file already available.
1558             if (empty($filerecord->filesize)) {
1559                 $filerecord->filesize = $existingfile->filesize;
1560             } else {
1561                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1562             }
1563         } else {
1564             // Attempt to get the result of last synchronisation for this reference.
1565             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1566                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1567             if ($lastcontent) {
1568                 $filerecord->contenthash = $lastcontent->contenthash;
1569                 $filerecord->filesize = $lastcontent->filesize;
1570             } else {
1571                 // External file doesn't have content in moodle.
1572                 // So we create an empty file for it.
1573                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null, $filerecord);
1574             }
1575         }
1577         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1579         try {
1580             $filerecord->id = $DB->insert_record('files', $filerecord);
1581         } catch (dml_exception $e) {
1582             if (!empty($newfile)) {
1583                 $this->move_to_trash($filerecord->contenthash);
1584             }
1585             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1586                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1587         }
1589         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1591         $transaction->allow_commit();
1593         // this will retrieve all reference information from DB as well
1594         return $this->get_file_by_id($filerecord->id);
1595     }
1597     /**
1598      * Creates new image file from existing.
1599      *
1600      * @param stdClass|array $filerecord object or array describing new file
1601      * @param int|stored_file $fid file id or stored file object
1602      * @param int $newwidth in pixels
1603      * @param int $newheight in pixels
1604      * @param bool $keepaspectratio whether or not keep aspect ratio
1605      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1606      * @return stored_file
1607      */
1608     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1609         if (!function_exists('imagecreatefromstring')) {
1610             //Most likely the GD php extension isn't installed
1611             //image conversion cannot succeed
1612             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1613         }
1615         if ($fid instanceof stored_file) {
1616             $fid = $fid->get_id();
1617         }
1619         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1621         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1622             throw new file_exception('storedfileproblem', 'File does not exist');
1623         }
1625         if (!$imageinfo = $file->get_imageinfo()) {
1626             throw new file_exception('storedfileproblem', 'File is not an image');
1627         }
1629         if (!isset($filerecord['filename'])) {
1630             $filerecord['filename'] = $file->get_filename();
1631         }
1633         if (!isset($filerecord['mimetype'])) {
1634             $filerecord['mimetype'] = $imageinfo['mimetype'];
1635         }
1637         $width    = $imageinfo['width'];
1638         $height   = $imageinfo['height'];
1640         if ($keepaspectratio) {
1641             if (0 >= $newwidth and 0 >= $newheight) {
1642                 // no sizes specified
1643                 $newwidth  = $width;
1644                 $newheight = $height;
1646             } else if (0 < $newwidth and 0 < $newheight) {
1647                 $xheight = ($newwidth*($height/$width));
1648                 if ($xheight < $newheight) {
1649                     $newheight = (int)$xheight;
1650                 } else {
1651                     $newwidth = (int)($newheight*($width/$height));
1652                 }
1654             } else if (0 < $newwidth) {
1655                 $newheight = (int)($newwidth*($height/$width));
1657             } else { //0 < $newheight
1658                 $newwidth = (int)($newheight*($width/$height));
1659             }
1661         } else {
1662             if (0 >= $newwidth) {
1663                 $newwidth = $width;
1664             }
1665             if (0 >= $newheight) {
1666                 $newheight = $height;
1667             }
1668         }
1670         // The original image.
1671         $img = imagecreatefromstring($file->get_content());
1673         // A new true color image where we will copy our original image.
1674         $newimg = imagecreatetruecolor($newwidth, $newheight);
1676         // Determine if the file supports transparency.
1677         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1679         // Maintain transparency.
1680         if ($hasalpha) {
1681             imagealphablending($newimg, true);
1683             // Get the current transparent index for the original image.
1684             $colour = imagecolortransparent($img);
1685             if ($colour == -1) {
1686                 // Set a transparent colour index if there's none.
1687                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1688                 // Save full alpha channel.
1689                 imagesavealpha($newimg, true);
1690             }
1691             imagecolortransparent($newimg, $colour);
1692             imagefill($newimg, 0, 0, $colour);
1693         }
1695         // Process the image to be output.
1696         if ($height != $newheight or $width != $newwidth) {
1697             // Resample if the dimensions differ from the original.
1698             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1699                 // weird
1700                 throw new file_exception('storedfileproblem', 'Can not resize image');
1701             }
1702             imagedestroy($img);
1703             $img = $newimg;
1705         } else if ($hasalpha) {
1706             // Just copy to the new image with the alpha channel.
1707             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1708                 // Weird.
1709                 throw new file_exception('storedfileproblem', 'Can not copy image');
1710             }
1711             imagedestroy($img);
1712             $img = $newimg;
1714         } else {
1715             // No particular processing needed for the original image.
1716             imagedestroy($newimg);
1717         }
1719         ob_start();
1720         switch ($filerecord['mimetype']) {
1721             case 'image/gif':
1722                 imagegif($img);
1723                 break;
1725             case 'image/jpeg':
1726                 if (is_null($quality)) {
1727                     imagejpeg($img);
1728                 } else {
1729                     imagejpeg($img, NULL, $quality);
1730                 }
1731                 break;
1733             case 'image/png':
1734                 $quality = (int)$quality;
1736                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1737                 // the latter of which can go to 100, we need to make sure that quality here is
1738                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1739                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1740                 imagepng($img, NULL, $quality, NULL);
1741                 break;
1743             default:
1744                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1745         }
1747         $content = ob_get_contents();
1748         ob_end_clean();
1749         imagedestroy($img);
1751         if (!$content) {
1752             throw new file_exception('storedfileproblem', 'Can not convert image');
1753         }
1755         return $this->create_file_from_string($filerecord, $content);
1756     }
1758     /**
1759      * Add file content to sha1 pool.
1760      *
1761      * @param string $pathname path to file
1762      * @param string|null $contenthash sha1 hash of content if known (performance only)
1763      * @param stdClass|null $newrecord New file record
1764      * @return array (contenthash, filesize, newfile)
1765      */
1766     public function add_file_to_pool($pathname, $contenthash = null, $newrecord = null) {
1767         $this->call_before_file_created_plugin_functions($newrecord, $pathname);
1768         return $this->filesystem->add_file_from_path($pathname, $contenthash);
1769     }
1771     /**
1772      * Add string content to sha1 pool.
1773      *
1774      * @param string $content file content - binary string
1775      * @return array (contenthash, filesize, newfile)
1776      */
1777     public function add_string_to_pool($content, $newrecord = null) {
1778         $this->call_before_file_created_plugin_functions($newrecord, null, $content);
1779         return $this->filesystem->add_file_from_string($content);
1780     }
1782     /**
1783      * before_file_created hook.
1784      *
1785      * @param stdClass|null $newrecord New file record.
1786      * @param string|null $pathname Path to file.
1787      * @param string|null $content File content.
1788      */
1789     protected function call_before_file_created_plugin_functions($newrecord, $pathname = null, $content = null) {
1790         $pluginsfunction = get_plugins_with_function('before_file_created');
1791         foreach ($pluginsfunction as $plugintype => $plugins) {
1792             foreach ($plugins as $pluginfunction) {
1793                 $pluginfunction($newrecord, ['pathname' => $pathname, 'content' => $content]);
1794             }
1795         }
1796     }
1798     /**
1799      * Serve file content using X-Sendfile header.
1800      * Please make sure that all headers are already sent
1801      * and the all access control checks passed.
1802      *
1803      * @param string $contenthash sah1 hash of the file content to be served
1804      * @return bool success
1805      */
1806     public function xsendfile($contenthash) {
1807         return $this->filesystem->xsendfile($contenthash);
1808     }
1810     /**
1811      * Content exists
1812      *
1813      * @param string $contenthash
1814      * @return bool
1815      * @deprecated since 3.3
1816      */
1817     public function content_exists($contenthash) {
1818         debugging('The content_exists function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1820         return false;
1821     }
1823     /**
1824      * Tries to recover missing content of file from trash.
1825      *
1826      * @param stored_file $file stored_file instance
1827      * @return bool success
1828      * @deprecated since 3.3
1829      */
1830     public function try_content_recovery($file) {
1831         debugging('The try_content_recovery function has been deprecated and should no longer be used.', DEBUG_DEVELOPER);
1833         return false;
1834     }
1836     /**
1837      * When user referring to a moodle file, we build the reference field
1838      *
1839      * @param array $params
1840      * @return string
1841      */
1842     public static function pack_reference($params) {
1843         $params = (array)$params;
1844         $reference = array();
1845         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1846         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1847         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1848         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1849         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1850         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1851         return base64_encode(serialize($reference));
1852     }
1854     /**
1855      * Unpack reference field
1856      *
1857      * @param string $str
1858      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1859      * @throws file_reference_exception if the $str does not have the expected format
1860      * @return array
1861      */
1862     public static function unpack_reference($str, $cleanparams = false) {
1863         $decoded = base64_decode($str, true);
1864         if ($decoded === false) {
1865             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1866         }
1867         $params = @unserialize($decoded); // hide E_NOTICE
1868         if ($params === false) {
1869             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1870         }
1871         if (is_array($params) && $cleanparams) {
1872             $params = array(
1873                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1874                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1875                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1876                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1877                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1878                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1879             );
1880         }
1881         return $params;
1882     }
1884     /**
1885      * Search through the server files.
1886      *
1887      * The query parameter will be used in conjuction with the SQL directive
1888      * LIKE, so include '%' in it if you need to. This search will always ignore
1889      * user files and directories. Note that the search is case insensitive.
1890      *
1891      * This query can quickly become inefficient so use it sparignly.
1892      *
1893      * @param  string  $query The string used with SQL LIKE.
1894      * @param  integer $from  The offset to start the search at.
1895      * @param  integer $limit The maximum number of results.
1896      * @param  boolean $count When true this methods returns the number of results availabe,
1897      *                        disregarding the parameters $from and $limit.
1898      * @return int|array      Integer when count, otherwise array of stored_file objects.
1899      */
1900     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
1901         global $DB;
1902         $params = array(
1903             'contextlevel' => CONTEXT_USER,
1904             'directory' => '.',
1905             'query' => $query
1906         );
1908         if ($count) {
1909             $select = 'COUNT(1)';
1910         } else {
1911             $select = self::instance_sql_fields('f', 'r');
1912         }
1913         $like = $DB->sql_like('f.filename', ':query', false);
1915         $sql = "SELECT $select
1916                   FROM {files} f
1917              LEFT JOIN {files_reference} r
1918                     ON f.referencefileid = r.id
1919                   JOIN {context} c
1920                     ON f.contextid = c.id
1921                  WHERE c.contextlevel <> :contextlevel
1922                    AND f.filename <> :directory
1923                    AND " . $like . "";
1925         if ($count) {
1926             return $DB->count_records_sql($sql, $params);
1927         }
1929         $sql .= " ORDER BY f.filename";
1931         $result = array();
1932         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
1933         foreach ($filerecords as $filerecord) {
1934             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1935         }
1936         $filerecords->close();
1938         return $result;
1939     }
1941     /**
1942      * Returns all aliases that refer to some stored_file via the given reference
1943      *
1944      * All repositories that provide access to a stored_file are expected to use
1945      * {@link self::pack_reference()}. This method can't be used if the given reference
1946      * does not use this format or if you are looking for references to an external file
1947      * (for example it can't be used to search for all aliases that refer to a given
1948      * Dropbox or Box.net file).
1949      *
1950      * Aliases in user draft areas are excluded from the returned list.
1951      *
1952      * @param string $reference identification of the referenced file
1953      * @return array of stored_file indexed by its pathnamehash
1954      */
1955     public function search_references($reference) {
1956         global $DB;
1958         if (is_null($reference)) {
1959             throw new coding_exception('NULL is not a valid reference to an external file');
1960         }
1962         // Give {@link self::unpack_reference()} a chance to throw exception if the
1963         // reference is not in a valid format.
1964         self::unpack_reference($reference);
1966         $referencehash = sha1($reference);
1968         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1969                   FROM {files} f
1970                   JOIN {files_reference} r ON f.referencefileid = r.id
1971                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1972                  WHERE r.referencehash = ?
1973                        AND (f.component <> ? OR f.filearea <> ?)";
1975         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1976         $files = array();
1977         foreach ($rs as $filerecord) {
1978             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1979         }
1981         return $files;
1982     }
1984     /**
1985      * Returns the number of aliases that refer to some stored_file via the given reference
1986      *
1987      * All repositories that provide access to a stored_file are expected to use
1988      * {@link self::pack_reference()}. This method can't be used if the given reference
1989      * does not use this format or if you are looking for references to an external file
1990      * (for example it can't be used to count aliases that refer to a given Dropbox or
1991      * Box.net file).
1992      *
1993      * Aliases in user draft areas are not counted.
1994      *
1995      * @param string $reference identification of the referenced file
1996      * @return int
1997      */
1998     public function search_references_count($reference) {
1999         global $DB;
2001         if (is_null($reference)) {
2002             throw new coding_exception('NULL is not a valid reference to an external file');
2003         }
2005         // Give {@link self::unpack_reference()} a chance to throw exception if the
2006         // reference is not in a valid format.
2007         self::unpack_reference($reference);
2009         $referencehash = sha1($reference);
2011         $sql = "SELECT COUNT(f.id)
2012                   FROM {files} f
2013                   JOIN {files_reference} r ON f.referencefileid = r.id
2014                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2015                  WHERE r.referencehash = ?
2016                        AND (f.component <> ? OR f.filearea <> ?)";
2018         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2019     }
2021     /**
2022      * Returns all aliases that link to the given stored_file
2023      *
2024      * Aliases in user draft areas are excluded from the returned list.
2025      *
2026      * @param stored_file $storedfile
2027      * @return array of stored_file
2028      */
2029     public function get_references_by_storedfile(stored_file $storedfile) {
2030         global $DB;
2032         $params = array();
2033         $params['contextid'] = $storedfile->get_contextid();
2034         $params['component'] = $storedfile->get_component();
2035         $params['filearea']  = $storedfile->get_filearea();
2036         $params['itemid']    = $storedfile->get_itemid();
2037         $params['filename']  = $storedfile->get_filename();
2038         $params['filepath']  = $storedfile->get_filepath();
2040         return $this->search_references(self::pack_reference($params));
2041     }
2043     /**
2044      * Returns the number of aliases that link to the given stored_file
2045      *
2046      * Aliases in user draft areas are not counted.
2047      *
2048      * @param stored_file $storedfile
2049      * @return int
2050      */
2051     public function get_references_count_by_storedfile(stored_file $storedfile) {
2052         global $DB;
2054         $params = array();
2055         $params['contextid'] = $storedfile->get_contextid();
2056         $params['component'] = $storedfile->get_component();
2057         $params['filearea']  = $storedfile->get_filearea();
2058         $params['itemid']    = $storedfile->get_itemid();
2059         $params['filename']  = $storedfile->get_filename();
2060         $params['filepath']  = $storedfile->get_filepath();
2062         return $this->search_references_count(self::pack_reference($params));
2063     }
2065     /**
2066      * Updates all files that are referencing this file with the new contenthash
2067      * and filesize
2068      *
2069      * @param stored_file $storedfile
2070      */
2071     public function update_references_to_storedfile(stored_file $storedfile) {
2072         global $CFG, $DB;
2073         $params = array();
2074         $params['contextid'] = $storedfile->get_contextid();
2075         $params['component'] = $storedfile->get_component();
2076         $params['filearea']  = $storedfile->get_filearea();
2077         $params['itemid']    = $storedfile->get_itemid();
2078         $params['filename']  = $storedfile->get_filename();
2079         $params['filepath']  = $storedfile->get_filepath();
2080         $reference = self::pack_reference($params);
2081         $referencehash = sha1($reference);
2083         $sql = "SELECT repositoryid, id FROM {files_reference}
2084                  WHERE referencehash = ?";
2085         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2087         $now = time();
2088         foreach ($rs as $record) {
2089             $this->update_references($record->id, $now, null,
2090                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2091         }
2092         $rs->close();
2093     }
2095     /**
2096      * Convert file alias to local file
2097      *
2098      * @throws moodle_exception if file could not be downloaded
2099      *
2100      * @param stored_file $storedfile a stored_file instances
2101      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2102      * @return stored_file stored_file
2103      */
2104     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2105         global $CFG;
2106         $storedfile->import_external_file_contents($maxbytes);
2107         $storedfile->delete_reference();
2108         return $storedfile;
2109     }
2111     /**
2112      * Return mimetype by given file pathname.
2113      *
2114      * If file has a known extension, we return the mimetype based on extension.
2115      * Otherwise (when possible) we try to get the mimetype from file contents.
2116      *
2117      * @param string $fullpath Full path to the file on disk
2118      * @param string $filename Correct file name with extension, if omitted will be taken from $path
2119      * @return string
2120      */
2121     public static function mimetype($fullpath, $filename = null) {
2122         if (empty($filename)) {
2123             $filename = $fullpath;
2124         }
2126         // The mimeinfo function determines the mimetype purely based on the file extension.
2127         $type = mimeinfo('type', $filename);
2129         if ($type === 'document/unknown') {
2130             // The type is unknown. Inspect the file now.
2131             $type = self::mimetype_from_file($fullpath);
2132         }
2133         return $type;
2134     }
2136     /**
2137      * Inspect a file on disk for it's mimetype.
2138      *
2139      * @param string $fullpath Path to file on disk
2140      * @return string The mimetype
2141      */
2142     public static function mimetype_from_file($fullpath) {
2143         if (file_exists($fullpath)) {
2144             // The type is unknown. Attempt to look up the file type now.
2145             $finfo = new finfo(FILEINFO_MIME_TYPE);
2146             return mimeinfo_from_type('type', $finfo->file($fullpath));
2147         }
2149         return 'document/unknown';
2150     }
2152     /**
2153      * Cron cleanup job.
2154      */
2155     public function cron() {
2156         global $CFG, $DB;
2157         require_once($CFG->libdir.'/cronlib.php');
2159         // find out all stale draft areas (older than 4 days) and purge them
2160         // those are identified by time stamp of the /. root dir
2161         mtrace('Deleting old draft files... ', '');
2162         cron_trace_time_and_memory();
2163         $old = time() - 60*60*24*4;
2164         $sql = "SELECT *
2165                   FROM {files}
2166                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2167                        AND timecreated < :old";
2168         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2169         foreach ($rs as $dir) {
2170             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2171         }
2172         $rs->close();
2173         mtrace('done.');
2175         // remove orphaned preview files (that is files in the core preview filearea without
2176         // the existing original file)
2177         mtrace('Deleting orphaned preview files... ', '');
2178         cron_trace_time_and_memory();
2179         $sql = "SELECT p.*
2180                   FROM {files} p
2181              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2182                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2183                        AND o.id IS NULL";
2184         $syscontext = context_system::instance();
2185         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2186         foreach ($rs as $orphan) {
2187             $file = $this->get_file_instance($orphan);
2188             if (!$file->is_directory()) {
2189                 $file->delete();
2190             }
2191         }
2192         $rs->close();
2193         mtrace('done.');
2195         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2196         // the existing original file).
2197         mtrace('Deleting orphaned document conversion files... ', '');
2198         cron_trace_time_and_memory();
2199         $sql = "SELECT p.*
2200                   FROM {files} p
2201              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2202                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
2203                        AND o.id IS NULL";
2204         $syscontext = context_system::instance();
2205         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2206         foreach ($rs as $orphan) {
2207             $file = $this->get_file_instance($orphan);
2208             if (!$file->is_directory()) {
2209                 $file->delete();
2210             }
2211         }
2212         $rs->close();
2213         mtrace('done.');
2215         // remove trash pool files once a day
2216         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2217         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2218             require_once($CFG->libdir.'/filelib.php');
2219             // Delete files that are associated with a context that no longer exists.
2220             mtrace('Cleaning up files from deleted contexts... ', '');
2221             cron_trace_time_and_memory();
2222             $sql = "SELECT DISTINCT f.contextid
2223                     FROM {files} f
2224                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2225                     WHERE c.id IS NULL";
2226             $rs = $DB->get_recordset_sql($sql);
2227             if ($rs->valid()) {
2228                 $fs = get_file_storage();
2229                 foreach ($rs as $ctx) {
2230                     $fs->delete_area_files($ctx->contextid);
2231                 }
2232             }
2233             $rs->close();
2234             mtrace('done.');
2236             mtrace('Call filesystem cron tasks.', '');
2237             cron_trace_time_and_memory();
2238             $this->filesystem->cron();
2239             mtrace('done.');
2240         }
2241     }
2243     /**
2244      * Get the sql formated fields for a file instance to be created from a
2245      * {files} and {files_refernece} join.
2246      *
2247      * @param string $filesprefix the table prefix for the {files} table
2248      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2249      * @return string the sql to go after a SELECT
2250      */
2251     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2252         // Note, these fieldnames MUST NOT overlap between the two tables,
2253         // else problems like MDL-33172 occur.
2254         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2255             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2256             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2258         $referencefields = array('repositoryid' => 'repositoryid',
2259             'reference' => 'reference',
2260             'lastsync' => 'referencelastsync');
2262         // id is specifically named to prevent overlaping between the two tables.
2263         $fields = array();
2264         $fields[] = $filesprefix.'.id AS id';
2265         foreach ($filefields as $field) {
2266             $fields[] = "{$filesprefix}.{$field}";
2267         }
2269         foreach ($referencefields as $field => $alias) {
2270             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2271         }
2273         return implode(', ', $fields);
2274     }
2276     /**
2277      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2278      *
2279      * If the record already exists, its id is returned. If there is no such record yet,
2280      * new one is created (using the lastsync provided, too) and its id is returned.
2281      *
2282      * @param int $repositoryid
2283      * @param string $reference
2284      * @param int $lastsync
2285      * @param int $lifetime argument not used any more
2286      * @return int
2287      */
2288     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2289         global $DB;
2291         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2293         if ($id !== false) {
2294             // bah, that was easy
2295             return $id;
2296         }
2298         // no such record yet, create one
2299         try {
2300             $id = $DB->insert_record('files_reference', array(
2301                 'repositoryid'  => $repositoryid,
2302                 'reference'     => $reference,
2303                 'referencehash' => sha1($reference),
2304                 'lastsync'      => $lastsync));
2305         } catch (dml_exception $e) {
2306             // if inserting the new record failed, chances are that the race condition has just
2307             // occured and the unique index did not allow to create the second record with the same
2308             // repositoryid + reference combo
2309             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2310         }
2312         return $id;
2313     }
2315     /**
2316      * Returns the id of the record in {files_reference} that matches the passed parameters
2317      *
2318      * Depending on the required strictness, false can be returned. The behaviour is consistent
2319      * with standard DML methods.
2320      *
2321      * @param int $repositoryid
2322      * @param string $reference
2323      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2324      * @return int|bool
2325      */
2326     private function get_referencefileid($repositoryid, $reference, $strictness) {
2327         global $DB;
2329         return $DB->get_field('files_reference', 'id',
2330             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2331     }
2333     /**
2334      * Updates a reference to the external resource and all files that use it
2335      *
2336      * This function is called after synchronisation of an external file and updates the
2337      * contenthash, filesize and status of all files that reference this external file
2338      * as well as time last synchronised.
2339      *
2340      * @param int $referencefileid
2341      * @param int $lastsync
2342      * @param int $lifetime argument not used any more, liefetime is returned by repository
2343      * @param string $contenthash
2344      * @param int $filesize
2345      * @param int $status 0 if ok or 666 if source is missing
2346      * @param int $timemodified last time modified of the source, if known
2347      */
2348     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2349         global $DB;
2350         $referencefileid = clean_param($referencefileid, PARAM_INT);
2351         $lastsync = clean_param($lastsync, PARAM_INT);
2352         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2353         $filesize = clean_param($filesize, PARAM_INT);
2354         $status = clean_param($status, PARAM_INT);
2355         $params = array('contenthash' => $contenthash,
2356                     'filesize' => $filesize,
2357                     'status' => $status,
2358                     'referencefileid' => $referencefileid,
2359                     'timemodified' => $timemodified);
2360         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2361             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2362             WHERE referencefileid = :referencefileid', $params);
2363         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2364         $DB->update_record('files_reference', (object)$data);
2365     }
2367     /**
2368      * Calculate and return the contenthash of the supplied file.
2369      *
2370      * @param   string $filepath The path to the file on disk
2371      * @return  string The file's content hash
2372      */
2373     public static function hash_from_path($filepath) {
2374         return sha1_file($filepath);
2375     }
2377     /**
2378      * Calculate and return the contenthash of the supplied content.
2379      *
2380      * @param   string $content The file content
2381      * @return  string The file's content hash
2382      */
2383     public static function hash_from_string($content) {
2384         return sha1($content);
2385     }