e4b1dde181f0e64f53a54498f44476cccc3c048e
[moodle.git] / lib / filestorage / file_storage.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Core file storage class definition.
20  *
21  * @package   core_files
22  * @copyright 2008 Petr Skoda {@link http://skodak.org}
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/stored_file.php");
30 /**
31  * File storage class used for low level access to stored files.
32  *
33  * Only owner of file area may use this class to access own files,
34  * for example only code in mod/assignment/* may access assignment
35  * attachments. When some other part of moodle needs to access
36  * files of modules it has to use file_browser class instead or there
37  * has to be some callback API.
38  *
39  * @package   core_files
40  * @category  files
41  * @copyright 2008 Petr Skoda {@link http://skodak.org}
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  * @since     Moodle 2.0
44  */
45 class file_storage {
46     /** @var string Directory with file contents */
47     private $filedir;
48     /** @var string Contents of deleted files not needed any more */
49     private $trashdir;
50     /** @var string tempdir */
51     private $tempdir;
52     /** @var int Permissions for new directories */
53     private $dirpermissions;
54     /** @var int Permissions for new files */
55     private $filepermissions;
56     /** @var array List of formats supported by unoconv */
57     private $unoconvformats;
60     /**
61      * Constructor - do not use directly use {@link get_file_storage()} call instead.
62      *
63      * @param string $filedir full path to pool directory
64      * @param string $trashdir temporary storage of deleted area
65      * @param string $tempdir temporary storage of various files
66      * @param int $dirpermissions new directory permissions
67      * @param int $filepermissions new file permissions
68      */
69     public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
70         global $CFG;
72         $this->filedir         = $filedir;
73         $this->trashdir        = $trashdir;
74         $this->tempdir         = $tempdir;
75         $this->dirpermissions  = $dirpermissions;
76         $this->filepermissions = $filepermissions;
78         // make sure the file pool directory exists
79         if (!is_dir($this->filedir)) {
80             if (!mkdir($this->filedir, $this->dirpermissions, true)) {
81                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
82             }
83             // place warning file in file pool root
84             if (!file_exists($this->filedir.'/warning.txt')) {
85                 file_put_contents($this->filedir.'/warning.txt',
86                                   'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
87                 chmod($this->filedir.'/warning.txt', $CFG->filepermissions);
88             }
89         }
90         // make sure the file pool directory exists
91         if (!is_dir($this->trashdir)) {
92             if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
93                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
94             }
95         }
96     }
98     /**
99      * Calculates sha1 hash of unique full path name information.
100      *
101      * This hash is a unique file identifier - it is used to improve
102      * performance and overcome db index size limits.
103      *
104      * @param int $contextid context ID
105      * @param string $component component
106      * @param string $filearea file area
107      * @param int $itemid item ID
108      * @param string $filepath file path
109      * @param string $filename file name
110      * @return string sha1 hash
111      */
112     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
113         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
114     }
116     /**
117      * Does this file exist?
118      *
119      * @param int $contextid context ID
120      * @param string $component component
121      * @param string $filearea file area
122      * @param int $itemid item ID
123      * @param string $filepath file path
124      * @param string $filename file name
125      * @return bool
126      */
127     public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
128         $filepath = clean_param($filepath, PARAM_PATH);
129         $filename = clean_param($filename, PARAM_FILE);
131         if ($filename === '') {
132             $filename = '.';
133         }
135         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
136         return $this->file_exists_by_hash($pathnamehash);
137     }
139     /**
140      * Whether or not the file exist
141      *
142      * @param string $pathnamehash path name hash
143      * @return bool
144      */
145     public function file_exists_by_hash($pathnamehash) {
146         global $DB;
148         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
149     }
151     /**
152      * Create instance of file class from database record.
153      *
154      * @param stdClass $filerecord record from the files table left join files_reference table
155      * @return stored_file instance of file abstraction class
156      */
157     public function get_file_instance(stdClass $filerecord) {
158         $storedfile = new stored_file($this, $filerecord, $this->filedir);
159         return $storedfile;
160     }
162     /**
163      * Get converted document.
164      *
165      * Get an alternate version of the specified document, if it is possible to convert.
166      *
167      * @param stored_file $file the file we want to preview
168      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
169      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
170      */
171     public function get_converted_document(stored_file $file, $format) {
173         $context = context_system::instance();
174         $path = '/' . $format . '/';
175         $conversion = $this->get_file($context->id, 'core', 'documentconversion', 0, $path, $file->get_contenthash());
177         if (!$conversion) {
178             $conversion = $this->create_converted_document($file, $format);
179             if (!$conversion) {
180                 return false;
181             }
182         }
184         return $conversion;
185     }
187     /**
188      * Verify the format is supported.
189      *
190      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
191      * @return bool - True if the format is supported for input.
192      */
193     protected function is_format_supported_by_unoconv($format) {
194         global $CFG;
196         if (!isset($this->unoconvformats)) {
197             // Ask unoconv for it's list of supported document formats.
198             $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' --show';
199             $pipes = array();
200             $pipesspec = array(2 => array('pipe', 'w'));
201             $proc = proc_open($cmd, $pipesspec, $pipes);
202             $programoutput = stream_get_contents($pipes[2]);
203             fclose($pipes[2]);
204             proc_close($proc);
205             $matches = array();
206             preg_match_all('/\[\.(.*)\]/', $programoutput, $matches);
208             $this->unoconvformats = $matches[1];
209             $this->unoconvformats = array_unique($this->unoconvformats);
210         }
212         $sanitized = trim(core_text::strtolower($format));
213         return in_array($sanitized, $this->unoconvformats);
214     }
217     /**
218      * Perform a file format conversion on the specified document.
219      *
220      * @param stored_file $file the file we want to preview
221      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
222      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
223      */
224     protected function create_converted_document(stored_file $file, $format) {
225         global $CFG;
227         if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
228             // No conversions are possible, sorry.
229             return false;
230         }
232         $fileextension = core_text::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
233         if (!self::is_format_supported_by_unoconv($fileextension)) {
234             return false;
235         }
237         if (!self::is_format_supported_by_unoconv($format)) {
238             return false;
239         }
241         // Copy the file to the local tmp dir.
242         $tmp = make_request_directory();
243         $localfilename = $file->get_filename();
244         // Safety.
245         $localfilename = clean_param($localfilename, PARAM_FILE);
247         $filename = $tmp . '/' . $localfilename;
248         $file->copy_content_to($filename);
250         $newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format;
252         // Safety.
253         $newtmpfile = $tmp . '/' . clean_param($newtmpfile, PARAM_FILE);
255         $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' .
256                escapeshellarg('-f') . ' ' .
257                escapeshellarg($format) . ' ' .
258                escapeshellarg('-o') . ' ' .
259                escapeshellarg($newtmpfile) . ' ' .
260                escapeshellarg($filename);
262         $e = file_exists($filename);
263         $output = null;
264         $currentdir = getcwd();
265         chdir($tmp);
266         $result = exec($cmd, $output);
267         chdir($currentdir);
268         if (!file_exists($newtmpfile)) {
269             return false;
270         }
272         $context = context_system::instance();
273         $record = array(
274             'contextid' => $context->id,
275             'component' => 'core',
276             'filearea'  => 'documentconversion',
277             'itemid'    => 0,
278             'filepath'  => '/' . $format . '/',
279             'filename'  => $file->get_contenthash(),
280         );
282         return $this->create_file_from_pathname($record, $newtmpfile);
283     }
285     /**
286      * Returns an image file that represent the given stored file as a preview
287      *
288      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
289      * future, the support for other mimetypes can be added, too (eg. generate an image
290      * preview of PDF, text documents etc).
291      *
292      * @param stored_file $file the file we want to preview
293      * @param string $mode preview mode, eg. 'thumb'
294      * @return stored_file|bool false if unable to create the preview, stored file otherwise
295      */
296     public function get_file_preview(stored_file $file, $mode) {
298         $context = context_system::instance();
299         $path = '/' . trim($mode, '/') . '/';
300         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
302         if (!$preview) {
303             $preview = $this->create_file_preview($file, $mode);
304             if (!$preview) {
305                 return false;
306             }
307         }
309         return $preview;
310     }
312     /**
313      * Return an available file name.
314      *
315      * This will return the next available file name in the area, adding/incrementing a suffix
316      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
317      *
318      * If the file name passed is available without modification, it is returned as is.
319      *
320      * @param int $contextid context ID.
321      * @param string $component component.
322      * @param string $filearea file area.
323      * @param int $itemid area item ID.
324      * @param string $filepath the file path.
325      * @param string $filename the file name.
326      * @return string available file name.
327      * @throws coding_exception if the file name is invalid.
328      * @since Moodle 2.5
329      */
330     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
331         global $DB;
333         // Do not accept '.' or an empty file name (zero is acceptable).
334         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
335             throw new coding_exception('Invalid file name passed', $filename);
336         }
338         // The file does not exist, we return the same file name.
339         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
340             return $filename;
341         }
343         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
344         $pathinfo = pathinfo($filename);
345         $basename = $pathinfo['filename'];
346         $matches = array();
347         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
348             $basename = $matches[1];
349         }
351         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
352         if (isset($pathinfo['extension'])) {
353             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
354         }
356         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
357         $filenamelen = $DB->sql_length('f.filename');
358         $sql = "SELECT filename
359                 FROM {files} f
360                 WHERE
361                     f.contextid = :contextid AND
362                     f.component = :component AND
363                     f.filearea = :filearea AND
364                     f.itemid = :itemid AND
365                     f.filepath = :filepath AND
366                     $filenamelikesql
367                 ORDER BY
368                     $filenamelen DESC,
369                     f.filename DESC";
370         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
371                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
372         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
374         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
375         // would both be returned, but only the one only containing digits should be used.
376         $number = 1;
377         foreach ($results as $result) {
378             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
379             $matches = array();
380             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
381                 $number = $matches[2] + 1;
382                 break;
383             }
384         }
386         // Constructing the new filename.
387         $newfilename = $basename . ' (' . $number . ')';
388         if (isset($pathinfo['extension'])) {
389             $newfilename .= '.' . $pathinfo['extension'];
390         }
392         return $newfilename;
393     }
395     /**
396      * Return an available directory name.
397      *
398      * This will return the next available directory name in the area, adding/incrementing a suffix
399      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
400      *
401      * If the file path passed is available without modification, it is returned as is.
402      *
403      * @param int $contextid context ID.
404      * @param string $component component.
405      * @param string $filearea file area.
406      * @param int $itemid area item ID.
407      * @param string $suggestedpath the suggested file path.
408      * @return string available file path
409      * @since Moodle 2.5
410      */
411     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
412         global $DB;
414         // Ensure suggestedpath has trailing '/'
415         $suggestedpath = rtrim($suggestedpath, '/'). '/';
417         // The directory does not exist, we return the same file path.
418         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
419             return $suggestedpath;
420         }
422         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
423         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
424             $suggestedpath = $matches[1]. '/';
425         }
427         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
429         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
430         $filepathlen = $DB->sql_length('f.filepath');
431         $sql = "SELECT filepath
432                 FROM {files} f
433                 WHERE
434                     f.contextid = :contextid AND
435                     f.component = :component AND
436                     f.filearea = :filearea AND
437                     f.itemid = :itemid AND
438                     f.filename = :filename AND
439                     $filepathlikesql
440                 ORDER BY
441                     $filepathlen DESC,
442                     f.filepath DESC";
443         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
444                 'filename' => '.', 'filepathlike' => $filepathlike);
445         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
447         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
448         // would both be returned, but only the one only containing digits should be used.
449         $number = 1;
450         foreach ($results as $result) {
451             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
452                 $number = (int)($matches[1]) + 1;
453                 break;
454             }
455         }
457         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
458     }
460     /**
461      * Generates a preview image for the stored file
462      *
463      * @param stored_file $file the file we want to preview
464      * @param string $mode preview mode, eg. 'thumb'
465      * @return stored_file|bool the newly created preview file or false
466      */
467     protected function create_file_preview(stored_file $file, $mode) {
469         $mimetype = $file->get_mimetype();
471         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
472             // make a preview of the image
473             $data = $this->create_imagefile_preview($file, $mode);
475         } else {
476             // unable to create the preview of this mimetype yet
477             return false;
478         }
480         if (empty($data)) {
481             return false;
482         }
484         $context = context_system::instance();
485         $record = array(
486             'contextid' => $context->id,
487             'component' => 'core',
488             'filearea'  => 'preview',
489             'itemid'    => 0,
490             'filepath'  => '/' . trim($mode, '/') . '/',
491             'filename'  => $file->get_contenthash(),
492         );
494         $imageinfo = getimagesizefromstring($data);
495         if ($imageinfo) {
496             $record['mimetype'] = $imageinfo['mime'];
497         }
499         return $this->create_file_from_string($record, $data);
500     }
502     /**
503      * Generates a preview for the stored image file
504      *
505      * @param stored_file $file the image we want to preview
506      * @param string $mode preview mode, eg. 'thumb'
507      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
508      */
509     protected function create_imagefile_preview(stored_file $file, $mode) {
510         global $CFG;
511         require_once($CFG->libdir.'/gdlib.php');
513         if ($mode === 'tinyicon') {
514             $data = $file->generate_image_thumbnail(24, 24);
516         } else if ($mode === 'thumb') {
517             $data = $file->generate_image_thumbnail(90, 90);
519         } else if ($mode === 'bigthumb') {
520             $data = $file->generate_image_thumbnail(250, 250);
522         } else {
523             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
524         }
526         return $data;
527     }
529     /**
530      * Fetch file using local file id.
531      *
532      * Please do not rely on file ids, it is usually easier to use
533      * pathname hashes instead.
534      *
535      * @param int $fileid file ID
536      * @return stored_file|bool stored_file instance if exists, false if not
537      */
538     public function get_file_by_id($fileid) {
539         global $DB;
541         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
542                   FROM {files} f
543              LEFT JOIN {files_reference} r
544                        ON f.referencefileid = r.id
545                  WHERE f.id = ?";
546         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
547             return $this->get_file_instance($filerecord);
548         } else {
549             return false;
550         }
551     }
553     /**
554      * Fetch file using local file full pathname hash
555      *
556      * @param string $pathnamehash path name hash
557      * @return stored_file|bool stored_file instance if exists, false if not
558      */
559     public function get_file_by_hash($pathnamehash) {
560         global $DB;
562         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
563                   FROM {files} f
564              LEFT JOIN {files_reference} r
565                        ON f.referencefileid = r.id
566                  WHERE f.pathnamehash = ?";
567         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
568             return $this->get_file_instance($filerecord);
569         } else {
570             return false;
571         }
572     }
574     /**
575      * Fetch locally stored file.
576      *
577      * @param int $contextid context ID
578      * @param string $component component
579      * @param string $filearea file area
580      * @param int $itemid item ID
581      * @param string $filepath file path
582      * @param string $filename file name
583      * @return stored_file|bool stored_file instance if exists, false if not
584      */
585     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
586         $filepath = clean_param($filepath, PARAM_PATH);
587         $filename = clean_param($filename, PARAM_FILE);
589         if ($filename === '') {
590             $filename = '.';
591         }
593         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
594         return $this->get_file_by_hash($pathnamehash);
595     }
597     /**
598      * Are there any files (or directories)
599      *
600      * @param int $contextid context ID
601      * @param string $component component
602      * @param string $filearea file area
603      * @param bool|int $itemid item id or false if all items
604      * @param bool $ignoredirs whether or not ignore directories
605      * @return bool empty
606      */
607     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
608         global $DB;
610         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
611         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
613         if ($itemid !== false) {
614             $params['itemid'] = $itemid;
615             $where .= " AND itemid = :itemid";
616         }
618         if ($ignoredirs) {
619             $sql = "SELECT 'x'
620                       FROM {files}
621                      WHERE $where AND filename <> '.'";
622         } else {
623             $sql = "SELECT 'x'
624                       FROM {files}
625                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
626         }
628         return !$DB->record_exists_sql($sql, $params);
629     }
631     /**
632      * Returns all files belonging to given repository
633      *
634      * @param int $repositoryid
635      * @param string $sort A fragment of SQL to use for sorting
636      */
637     public function get_external_files($repositoryid, $sort = '') {
638         global $DB;
639         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
640                   FROM {files} f
641              LEFT JOIN {files_reference} r
642                        ON f.referencefileid = r.id
643                  WHERE r.repositoryid = ?";
644         if (!empty($sort)) {
645             $sql .= " ORDER BY {$sort}";
646         }
648         $result = array();
649         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
650         foreach ($filerecords as $filerecord) {
651             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
652         }
653         return $result;
654     }
656     /**
657      * Returns all area files (optionally limited by itemid)
658      *
659      * @param int $contextid context ID
660      * @param string $component component
661      * @param string $filearea file area
662      * @param int $itemid item ID or all files if not specified
663      * @param string $sort A fragment of SQL to use for sorting
664      * @param bool $includedirs whether or not include directories
665      * @return stored_file[] array of stored_files indexed by pathanmehash
666      */
667     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
668         global $DB;
670         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
671         if ($itemid !== false) {
672             $itemidsql = ' AND f.itemid = :itemid ';
673             $conditions['itemid'] = $itemid;
674         } else {
675             $itemidsql = '';
676         }
678         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
679                   FROM {files} f
680              LEFT JOIN {files_reference} r
681                        ON f.referencefileid = r.id
682                  WHERE f.contextid = :contextid
683                        AND f.component = :component
684                        AND f.filearea = :filearea
685                        $itemidsql";
686         if (!empty($sort)) {
687             $sql .= " ORDER BY {$sort}";
688         }
690         $result = array();
691         $filerecords = $DB->get_records_sql($sql, $conditions);
692         foreach ($filerecords as $filerecord) {
693             if (!$includedirs and $filerecord->filename === '.') {
694                 continue;
695             }
696             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
697         }
698         return $result;
699     }
701     /**
702      * Returns array based tree structure of area files
703      *
704      * @param int $contextid context ID
705      * @param string $component component
706      * @param string $filearea file area
707      * @param int $itemid item ID
708      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
709      */
710     public function get_area_tree($contextid, $component, $filearea, $itemid) {
711         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
712         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
713         // first create directory structure
714         foreach ($files as $hash=>$dir) {
715             if (!$dir->is_directory()) {
716                 continue;
717             }
718             unset($files[$hash]);
719             if ($dir->get_filepath() === '/') {
720                 $result['dirfile'] = $dir;
721                 continue;
722             }
723             $parts = explode('/', trim($dir->get_filepath(),'/'));
724             $pointer =& $result;
725             foreach ($parts as $part) {
726                 if ($part === '') {
727                     continue;
728                 }
729                 if (!isset($pointer['subdirs'][$part])) {
730                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
731                 }
732                 $pointer =& $pointer['subdirs'][$part];
733             }
734             $pointer['dirfile'] = $dir;
735             unset($pointer);
736         }
737         foreach ($files as $hash=>$file) {
738             $parts = explode('/', trim($file->get_filepath(),'/'));
739             $pointer =& $result;
740             foreach ($parts as $part) {
741                 if ($part === '') {
742                     continue;
743                 }
744                 $pointer =& $pointer['subdirs'][$part];
745             }
746             $pointer['files'][$file->get_filename()] = $file;
747             unset($pointer);
748         }
749         $result = $this->sort_area_tree($result);
750         return $result;
751     }
753     /**
754      * Sorts the result of {@link file_storage::get_area_tree()}.
755      *
756      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
757      * @return array of sorted results
758      */
759     protected function sort_area_tree($tree) {
760         foreach ($tree as $key => &$value) {
761             if ($key == 'subdirs') {
762                 core_collator::ksort($value, core_collator::SORT_NATURAL);
763                 foreach ($value as $subdirname => &$subtree) {
764                     $subtree = $this->sort_area_tree($subtree);
765                 }
766             } else if ($key == 'files') {
767                 core_collator::ksort($value, core_collator::SORT_NATURAL);
768             }
769         }
770         return $tree;
771     }
773     /**
774      * Returns all files and optionally directories
775      *
776      * @param int $contextid context ID
777      * @param string $component component
778      * @param string $filearea file area
779      * @param int $itemid item ID
780      * @param int $filepath directory path
781      * @param bool $recursive include all subdirectories
782      * @param bool $includedirs include files and directories
783      * @param string $sort A fragment of SQL to use for sorting
784      * @return array of stored_files indexed by pathanmehash
785      */
786     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
787         global $DB;
789         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
790             return array();
791         }
793         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
795         if ($recursive) {
797             $dirs = $includedirs ? "" : "AND filename <> '.'";
798             $length = core_text::strlen($filepath);
800             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
801                       FROM {files} f
802                  LEFT JOIN {files_reference} r
803                            ON f.referencefileid = r.id
804                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
805                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
806                            AND f.id <> :dirid
807                            $dirs
808                            $orderby";
809             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
811             $files = array();
812             $dirs  = array();
813             $filerecords = $DB->get_records_sql($sql, $params);
814             foreach ($filerecords as $filerecord) {
815                 if ($filerecord->filename == '.') {
816                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
817                 } else {
818                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
819                 }
820             }
821             $result = array_merge($dirs, $files);
823         } else {
824             $result = array();
825             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
827             $length = core_text::strlen($filepath);
829             if ($includedirs) {
830                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
831                           FROM {files} f
832                      LEFT JOIN {files_reference} r
833                                ON f.referencefileid = r.id
834                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
835                                AND f.itemid = :itemid AND f.filename = '.'
836                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
837                                AND f.id <> :dirid
838                                $orderby";
839                 $reqlevel = substr_count($filepath, '/') + 1;
840                 $filerecords = $DB->get_records_sql($sql, $params);
841                 foreach ($filerecords as $filerecord) {
842                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
843                         continue;
844                     }
845                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
846                 }
847             }
849             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
850                       FROM {files} f
851                  LEFT JOIN {files_reference} r
852                            ON f.referencefileid = r.id
853                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
854                            AND f.filepath = :filepath AND f.filename <> '.'
855                            $orderby";
857             $filerecords = $DB->get_records_sql($sql, $params);
858             foreach ($filerecords as $filerecord) {
859                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
860             }
861         }
863         return $result;
864     }
866     /**
867      * Delete all area files (optionally limited by itemid).
868      *
869      * @param int $contextid context ID
870      * @param string $component component
871      * @param string $filearea file area or all areas in context if not specified
872      * @param int $itemid item ID or all files if not specified
873      * @return bool success
874      */
875     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
876         global $DB;
878         $conditions = array('contextid'=>$contextid);
879         if ($component !== false) {
880             $conditions['component'] = $component;
881         }
882         if ($filearea !== false) {
883             $conditions['filearea'] = $filearea;
884         }
885         if ($itemid !== false) {
886             $conditions['itemid'] = $itemid;
887         }
889         $filerecords = $DB->get_records('files', $conditions);
890         foreach ($filerecords as $filerecord) {
891             $this->get_file_instance($filerecord)->delete();
892         }
894         return true; // BC only
895     }
897     /**
898      * Delete all the files from certain areas where itemid is limited by an
899      * arbitrary bit of SQL.
900      *
901      * @param int $contextid the id of the context the files belong to. Must be given.
902      * @param string $component the owning component. Must be given.
903      * @param string $filearea the file area name. Must be given.
904      * @param string $itemidstest an SQL fragment that the itemid must match. Used
905      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
906      *      and may not used named parameters called contextid, component or filearea.
907      * @param array $params any query params used by $itemidstest.
908      */
909     public function delete_area_files_select($contextid, $component,
910             $filearea, $itemidstest, array $params = null) {
911         global $DB;
913         $where = "contextid = :contextid
914                 AND component = :component
915                 AND filearea = :filearea
916                 AND itemid $itemidstest";
917         $params['contextid'] = $contextid;
918         $params['component'] = $component;
919         $params['filearea'] = $filearea;
921         $filerecords = $DB->get_recordset_select('files', $where, $params);
922         foreach ($filerecords as $filerecord) {
923             $this->get_file_instance($filerecord)->delete();
924         }
925         $filerecords->close();
926     }
928     /**
929      * Delete all files associated with the given component.
930      *
931      * @param string $component the component owning the file
932      */
933     public function delete_component_files($component) {
934         global $DB;
936         $filerecords = $DB->get_recordset('files', array('component' => $component));
937         foreach ($filerecords as $filerecord) {
938             $this->get_file_instance($filerecord)->delete();
939         }
940         $filerecords->close();
941     }
943     /**
944      * Move all the files in a file area from one context to another.
945      *
946      * @param int $oldcontextid the context the files are being moved from.
947      * @param int $newcontextid the context the files are being moved to.
948      * @param string $component the plugin that these files belong to.
949      * @param string $filearea the name of the file area.
950      * @param int $itemid file item ID
951      * @return int the number of files moved, for information.
952      */
953     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
954         // Note, this code is based on some code that Petr wrote in
955         // forum_move_attachments in mod/forum/lib.php. I moved it here because
956         // I needed it in the question code too.
957         $count = 0;
959         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
960         foreach ($oldfiles as $oldfile) {
961             $filerecord = new stdClass();
962             $filerecord->contextid = $newcontextid;
963             $this->create_file_from_storedfile($filerecord, $oldfile);
964             $count += 1;
965         }
967         if ($count) {
968             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
969         }
971         return $count;
972     }
974     /**
975      * Recursively creates directory.
976      *
977      * @param int $contextid context ID
978      * @param string $component component
979      * @param string $filearea file area
980      * @param int $itemid item ID
981      * @param string $filepath file path
982      * @param int $userid the user ID
983      * @return bool success
984      */
985     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
986         global $DB;
988         // validate all parameters, we do not want any rubbish stored in database, right?
989         if (!is_number($contextid) or $contextid < 1) {
990             throw new file_exception('storedfileproblem', 'Invalid contextid');
991         }
993         $component = clean_param($component, PARAM_COMPONENT);
994         if (empty($component)) {
995             throw new file_exception('storedfileproblem', 'Invalid component');
996         }
998         $filearea = clean_param($filearea, PARAM_AREA);
999         if (empty($filearea)) {
1000             throw new file_exception('storedfileproblem', 'Invalid filearea');
1001         }
1003         if (!is_number($itemid) or $itemid < 0) {
1004             throw new file_exception('storedfileproblem', 'Invalid itemid');
1005         }
1007         $filepath = clean_param($filepath, PARAM_PATH);
1008         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1009             // path must start and end with '/'
1010             throw new file_exception('storedfileproblem', 'Invalid file path');
1011         }
1013         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1015         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1016             return $dir_info;
1017         }
1019         static $contenthash = null;
1020         if (!$contenthash) {
1021             $this->add_string_to_pool('');
1022             $contenthash = sha1('');
1023         }
1025         $now = time();
1027         $dir_record = new stdClass();
1028         $dir_record->contextid = $contextid;
1029         $dir_record->component = $component;
1030         $dir_record->filearea  = $filearea;
1031         $dir_record->itemid    = $itemid;
1032         $dir_record->filepath  = $filepath;
1033         $dir_record->filename  = '.';
1034         $dir_record->contenthash  = $contenthash;
1035         $dir_record->filesize  = 0;
1037         $dir_record->timecreated  = $now;
1038         $dir_record->timemodified = $now;
1039         $dir_record->mimetype     = null;
1040         $dir_record->userid       = $userid;
1042         $dir_record->pathnamehash = $pathnamehash;
1044         $DB->insert_record('files', $dir_record);
1045         $dir_info = $this->get_file_by_hash($pathnamehash);
1047         if ($filepath !== '/') {
1048             //recurse to parent dirs
1049             $filepath = trim($filepath, '/');
1050             $filepath = explode('/', $filepath);
1051             array_pop($filepath);
1052             $filepath = implode('/', $filepath);
1053             $filepath = ($filepath === '') ? '/' : "/$filepath/";
1054             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1055         }
1057         return $dir_info;
1058     }
1060     /**
1061      * Add new local file based on existing local file.
1062      *
1063      * @param stdClass|array $filerecord object or array describing changes
1064      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1065      * @return stored_file instance of newly created file
1066      */
1067     public function create_file_from_storedfile($filerecord, $fileorid) {
1068         global $DB;
1070         if ($fileorid instanceof stored_file) {
1071             $fid = $fileorid->get_id();
1072         } else {
1073             $fid = $fileorid;
1074         }
1076         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1078         unset($filerecord['id']);
1079         unset($filerecord['filesize']);
1080         unset($filerecord['contenthash']);
1081         unset($filerecord['pathnamehash']);
1083         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1084                   FROM {files} f
1085              LEFT JOIN {files_reference} r
1086                        ON f.referencefileid = r.id
1087                  WHERE f.id = ?";
1089         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1090             throw new file_exception('storedfileproblem', 'File does not exist');
1091         }
1093         unset($newrecord->id);
1095         foreach ($filerecord as $key => $value) {
1096             // validate all parameters, we do not want any rubbish stored in database, right?
1097             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1098                 throw new file_exception('storedfileproblem', 'Invalid contextid');
1099             }
1101             if ($key == 'component') {
1102                 $value = clean_param($value, PARAM_COMPONENT);
1103                 if (empty($value)) {
1104                     throw new file_exception('storedfileproblem', 'Invalid component');
1105                 }
1106             }
1108             if ($key == 'filearea') {
1109                 $value = clean_param($value, PARAM_AREA);
1110                 if (empty($value)) {
1111                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1112                 }
1113             }
1115             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1116                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1117             }
1120             if ($key == 'filepath') {
1121                 $value = clean_param($value, PARAM_PATH);
1122                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1123                     // path must start and end with '/'
1124                     throw new file_exception('storedfileproblem', 'Invalid file path');
1125                 }
1126             }
1128             if ($key == 'filename') {
1129                 $value = clean_param($value, PARAM_FILE);
1130                 if ($value === '') {
1131                     // path must start and end with '/'
1132                     throw new file_exception('storedfileproblem', 'Invalid file name');
1133                 }
1134             }
1136             if ($key === 'timecreated' or $key === 'timemodified') {
1137                 if (!is_number($value)) {
1138                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1139                 }
1140                 if ($value < 0) {
1141                     //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)
1142                     $value = 0;
1143                 }
1144             }
1146             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1147                 $value = clean_param($value, PARAM_INT);
1148             }
1150             $newrecord->$key = $value;
1151         }
1153         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1155         if ($newrecord->filename === '.') {
1156             // special case - only this function supports directories ;-)
1157             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1158             // update the existing directory with the new data
1159             $newrecord->id = $directory->get_id();
1160             $DB->update_record('files', $newrecord);
1161             return $this->get_file_instance($newrecord);
1162         }
1164         // note: referencefileid is copied from the original file so that
1165         // creating a new file from an existing alias creates new alias implicitly.
1166         // here we just check the database consistency.
1167         if (!empty($newrecord->repositoryid)) {
1168             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1169                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1170             }
1171         }
1173         try {
1174             $newrecord->id = $DB->insert_record('files', $newrecord);
1175         } catch (dml_exception $e) {
1176             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1177                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1178         }
1181         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1183         return $this->get_file_instance($newrecord);
1184     }
1186     /**
1187      * Add new local file.
1188      *
1189      * @param stdClass|array $filerecord object or array describing file
1190      * @param string $url the URL to the file
1191      * @param array $options {@link download_file_content()} options
1192      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1193      * @return stored_file
1194      */
1195     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1197         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1198         $filerecord = (object)$filerecord; // We support arrays too.
1200         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1201         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1202         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1203         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1204         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1205         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1206         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1208         if (!isset($filerecord->filename)) {
1209             $parts = explode('/', $url);
1210             $filename = array_pop($parts);
1211             $filerecord->filename = clean_param($filename, PARAM_FILE);
1212         }
1213         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1214         $filerecord->source = clean_param($source, PARAM_URL);
1216         if ($usetempfile) {
1217             check_dir_exists($this->tempdir);
1218             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1219             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1220             if ($content === false) {
1221                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1222             }
1223             try {
1224                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1225                 @unlink($tmpfile);
1226                 return $newfile;
1227             } catch (Exception $e) {
1228                 @unlink($tmpfile);
1229                 throw $e;
1230             }
1232         } else {
1233             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1234             if ($content === false) {
1235                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1236             }
1237             return $this->create_file_from_string($filerecord, $content);
1238         }
1239     }
1241     /**
1242      * Add new local file.
1243      *
1244      * @param stdClass|array $filerecord object or array describing file
1245      * @param string $pathname path to file or content of file
1246      * @return stored_file
1247      */
1248     public function create_file_from_pathname($filerecord, $pathname) {
1249         global $DB;
1251         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1252         $filerecord = (object)$filerecord; // We support arrays too.
1254         // validate all parameters, we do not want any rubbish stored in database, right?
1255         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1256             throw new file_exception('storedfileproblem', 'Invalid contextid');
1257         }
1259         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1260         if (empty($filerecord->component)) {
1261             throw new file_exception('storedfileproblem', 'Invalid component');
1262         }
1264         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1265         if (empty($filerecord->filearea)) {
1266             throw new file_exception('storedfileproblem', 'Invalid filearea');
1267         }
1269         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1270             throw new file_exception('storedfileproblem', 'Invalid itemid');
1271         }
1273         if (!empty($filerecord->sortorder)) {
1274             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1275                 $filerecord->sortorder = 0;
1276             }
1277         } else {
1278             $filerecord->sortorder = 0;
1279         }
1281         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1282         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1283             // path must start and end with '/'
1284             throw new file_exception('storedfileproblem', 'Invalid file path');
1285         }
1287         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1288         if ($filerecord->filename === '') {
1289             // filename must not be empty
1290             throw new file_exception('storedfileproblem', 'Invalid file name');
1291         }
1293         $now = time();
1294         if (isset($filerecord->timecreated)) {
1295             if (!is_number($filerecord->timecreated)) {
1296                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1297             }
1298             if ($filerecord->timecreated < 0) {
1299                 //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)
1300                 $filerecord->timecreated = 0;
1301             }
1302         } else {
1303             $filerecord->timecreated = $now;
1304         }
1306         if (isset($filerecord->timemodified)) {
1307             if (!is_number($filerecord->timemodified)) {
1308                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1309             }
1310             if ($filerecord->timemodified < 0) {
1311                 //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)
1312                 $filerecord->timemodified = 0;
1313             }
1314         } else {
1315             $filerecord->timemodified = $now;
1316         }
1318         $newrecord = new stdClass();
1320         $newrecord->contextid = $filerecord->contextid;
1321         $newrecord->component = $filerecord->component;
1322         $newrecord->filearea  = $filerecord->filearea;
1323         $newrecord->itemid    = $filerecord->itemid;
1324         $newrecord->filepath  = $filerecord->filepath;
1325         $newrecord->filename  = $filerecord->filename;
1327         $newrecord->timecreated  = $filerecord->timecreated;
1328         $newrecord->timemodified = $filerecord->timemodified;
1329         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1330         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1331         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1332         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1333         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1334         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1335         $newrecord->sortorder    = $filerecord->sortorder;
1337         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1339         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1341         try {
1342             $newrecord->id = $DB->insert_record('files', $newrecord);
1343         } catch (dml_exception $e) {
1344             if ($newfile) {
1345                 $this->deleted_file_cleanup($newrecord->contenthash);
1346             }
1347             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1348                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1349         }
1351         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1353         return $this->get_file_instance($newrecord);
1354     }
1356     /**
1357      * Add new local file.
1358      *
1359      * @param stdClass|array $filerecord object or array describing file
1360      * @param string $content content of file
1361      * @return stored_file
1362      */
1363     public function create_file_from_string($filerecord, $content) {
1364         global $DB;
1366         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1367         $filerecord = (object)$filerecord; // We support arrays too.
1369         // validate all parameters, we do not want any rubbish stored in database, right?
1370         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1371             throw new file_exception('storedfileproblem', 'Invalid contextid');
1372         }
1374         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1375         if (empty($filerecord->component)) {
1376             throw new file_exception('storedfileproblem', 'Invalid component');
1377         }
1379         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1380         if (empty($filerecord->filearea)) {
1381             throw new file_exception('storedfileproblem', 'Invalid filearea');
1382         }
1384         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1385             throw new file_exception('storedfileproblem', 'Invalid itemid');
1386         }
1388         if (!empty($filerecord->sortorder)) {
1389             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1390                 $filerecord->sortorder = 0;
1391             }
1392         } else {
1393             $filerecord->sortorder = 0;
1394         }
1396         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1397         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1398             // path must start and end with '/'
1399             throw new file_exception('storedfileproblem', 'Invalid file path');
1400         }
1402         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1403         if ($filerecord->filename === '') {
1404             // path must start and end with '/'
1405             throw new file_exception('storedfileproblem', 'Invalid file name');
1406         }
1408         $now = time();
1409         if (isset($filerecord->timecreated)) {
1410             if (!is_number($filerecord->timecreated)) {
1411                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1412             }
1413             if ($filerecord->timecreated < 0) {
1414                 //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)
1415                 $filerecord->timecreated = 0;
1416             }
1417         } else {
1418             $filerecord->timecreated = $now;
1419         }
1421         if (isset($filerecord->timemodified)) {
1422             if (!is_number($filerecord->timemodified)) {
1423                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1424             }
1425             if ($filerecord->timemodified < 0) {
1426                 //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)
1427                 $filerecord->timemodified = 0;
1428             }
1429         } else {
1430             $filerecord->timemodified = $now;
1431         }
1433         $newrecord = new stdClass();
1435         $newrecord->contextid = $filerecord->contextid;
1436         $newrecord->component = $filerecord->component;
1437         $newrecord->filearea  = $filerecord->filearea;
1438         $newrecord->itemid    = $filerecord->itemid;
1439         $newrecord->filepath  = $filerecord->filepath;
1440         $newrecord->filename  = $filerecord->filename;
1442         $newrecord->timecreated  = $filerecord->timecreated;
1443         $newrecord->timemodified = $filerecord->timemodified;
1444         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1445         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1446         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1447         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1448         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1449         $newrecord->sortorder    = $filerecord->sortorder;
1451         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1452         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1453         // get mimetype by magic bytes
1454         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1456         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1458         try {
1459             $newrecord->id = $DB->insert_record('files', $newrecord);
1460         } catch (dml_exception $e) {
1461             if ($newfile) {
1462                 $this->deleted_file_cleanup($newrecord->contenthash);
1463             }
1464             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1465                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1466         }
1468         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1470         return $this->get_file_instance($newrecord);
1471     }
1473     /**
1474      * Create a new alias/shortcut file from file reference information
1475      *
1476      * @param stdClass|array $filerecord object or array describing the new file
1477      * @param int $repositoryid the id of the repository that provides the original file
1478      * @param string $reference the information required by the repository to locate the original file
1479      * @param array $options options for creating the new file
1480      * @return stored_file
1481      */
1482     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1483         global $DB;
1485         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1486         $filerecord = (object)$filerecord; // We support arrays too.
1488         // validate all parameters, we do not want any rubbish stored in database, right?
1489         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1490             throw new file_exception('storedfileproblem', 'Invalid contextid');
1491         }
1493         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1494         if (empty($filerecord->component)) {
1495             throw new file_exception('storedfileproblem', 'Invalid component');
1496         }
1498         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1499         if (empty($filerecord->filearea)) {
1500             throw new file_exception('storedfileproblem', 'Invalid filearea');
1501         }
1503         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1504             throw new file_exception('storedfileproblem', 'Invalid itemid');
1505         }
1507         if (!empty($filerecord->sortorder)) {
1508             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1509                 $filerecord->sortorder = 0;
1510             }
1511         } else {
1512             $filerecord->sortorder = 0;
1513         }
1515         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1516         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1517         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1518         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1519         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1520         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1521         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1522         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1523             // Path must start and end with '/'.
1524             throw new file_exception('storedfileproblem', 'Invalid file path');
1525         }
1527         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1528         if ($filerecord->filename === '') {
1529             // Path must start and end with '/'.
1530             throw new file_exception('storedfileproblem', 'Invalid file name');
1531         }
1533         $now = time();
1534         if (isset($filerecord->timecreated)) {
1535             if (!is_number($filerecord->timecreated)) {
1536                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1537             }
1538             if ($filerecord->timecreated < 0) {
1539                 // 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)
1540                 $filerecord->timecreated = 0;
1541             }
1542         } else {
1543             $filerecord->timecreated = $now;
1544         }
1546         if (isset($filerecord->timemodified)) {
1547             if (!is_number($filerecord->timemodified)) {
1548                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1549             }
1550             if ($filerecord->timemodified < 0) {
1551                 // 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)
1552                 $filerecord->timemodified = 0;
1553             }
1554         } else {
1555             $filerecord->timemodified = $now;
1556         }
1558         $transaction = $DB->start_delegated_transaction();
1560         try {
1561             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1562         } catch (Exception $e) {
1563             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1564         }
1566         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1567             // there was specified the contenthash for a file already stored in moodle filepool
1568             if (empty($filerecord->filesize)) {
1569                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1570                 $filerecord->filesize = filesize($filepathname);
1571             } else {
1572                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1573             }
1574         } else {
1575             // atempt to get the result of last synchronisation for this reference
1576             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1577                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1578             if ($lastcontent) {
1579                 $filerecord->contenthash = $lastcontent->contenthash;
1580                 $filerecord->filesize = $lastcontent->filesize;
1581             } else {
1582                 // External file doesn't have content in moodle.
1583                 // So we create an empty file for it.
1584                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1585             }
1586         }
1588         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1590         try {
1591             $filerecord->id = $DB->insert_record('files', $filerecord);
1592         } catch (dml_exception $e) {
1593             if (!empty($newfile)) {
1594                 $this->deleted_file_cleanup($filerecord->contenthash);
1595             }
1596             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1597                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1598         }
1600         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1602         $transaction->allow_commit();
1604         // this will retrieve all reference information from DB as well
1605         return $this->get_file_by_id($filerecord->id);
1606     }
1608     /**
1609      * Creates new image file from existing.
1610      *
1611      * @param stdClass|array $filerecord object or array describing new file
1612      * @param int|stored_file $fid file id or stored file object
1613      * @param int $newwidth in pixels
1614      * @param int $newheight in pixels
1615      * @param bool $keepaspectratio whether or not keep aspect ratio
1616      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1617      * @return stored_file
1618      */
1619     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1620         if (!function_exists('imagecreatefromstring')) {
1621             //Most likely the GD php extension isn't installed
1622             //image conversion cannot succeed
1623             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1624         }
1626         if ($fid instanceof stored_file) {
1627             $fid = $fid->get_id();
1628         }
1630         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1632         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1633             throw new file_exception('storedfileproblem', 'File does not exist');
1634         }
1636         if (!$imageinfo = $file->get_imageinfo()) {
1637             throw new file_exception('storedfileproblem', 'File is not an image');
1638         }
1640         if (!isset($filerecord['filename'])) {
1641             $filerecord['filename'] = $file->get_filename();
1642         }
1644         if (!isset($filerecord['mimetype'])) {
1645             $filerecord['mimetype'] = $imageinfo['mimetype'];
1646         }
1648         $width    = $imageinfo['width'];
1649         $height   = $imageinfo['height'];
1651         if ($keepaspectratio) {
1652             if (0 >= $newwidth and 0 >= $newheight) {
1653                 // no sizes specified
1654                 $newwidth  = $width;
1655                 $newheight = $height;
1657             } else if (0 < $newwidth and 0 < $newheight) {
1658                 $xheight = ($newwidth*($height/$width));
1659                 if ($xheight < $newheight) {
1660                     $newheight = (int)$xheight;
1661                 } else {
1662                     $newwidth = (int)($newheight*($width/$height));
1663                 }
1665             } else if (0 < $newwidth) {
1666                 $newheight = (int)($newwidth*($height/$width));
1668             } else { //0 < $newheight
1669                 $newwidth = (int)($newheight*($width/$height));
1670             }
1672         } else {
1673             if (0 >= $newwidth) {
1674                 $newwidth = $width;
1675             }
1676             if (0 >= $newheight) {
1677                 $newheight = $height;
1678             }
1679         }
1681         // The original image.
1682         $img = imagecreatefromstring($file->get_content());
1684         // A new true color image where we will copy our original image.
1685         $newimg = imagecreatetruecolor($newwidth, $newheight);
1687         // Determine if the file supports transparency.
1688         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1690         // Maintain transparency.
1691         if ($hasalpha) {
1692             imagealphablending($newimg, true);
1694             // Get the current transparent index for the original image.
1695             $colour = imagecolortransparent($img);
1696             if ($colour == -1) {
1697                 // Set a transparent colour index if there's none.
1698                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1699                 // Save full alpha channel.
1700                 imagesavealpha($newimg, true);
1701             }
1702             imagecolortransparent($newimg, $colour);
1703             imagefill($newimg, 0, 0, $colour);
1704         }
1706         // Process the image to be output.
1707         if ($height != $newheight or $width != $newwidth) {
1708             // Resample if the dimensions differ from the original.
1709             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1710                 // weird
1711                 throw new file_exception('storedfileproblem', 'Can not resize image');
1712             }
1713             imagedestroy($img);
1714             $img = $newimg;
1716         } else if ($hasalpha) {
1717             // Just copy to the new image with the alpha channel.
1718             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1719                 // Weird.
1720                 throw new file_exception('storedfileproblem', 'Can not copy image');
1721             }
1722             imagedestroy($img);
1723             $img = $newimg;
1725         } else {
1726             // No particular processing needed for the original image.
1727             imagedestroy($newimg);
1728         }
1730         ob_start();
1731         switch ($filerecord['mimetype']) {
1732             case 'image/gif':
1733                 imagegif($img);
1734                 break;
1736             case 'image/jpeg':
1737                 if (is_null($quality)) {
1738                     imagejpeg($img);
1739                 } else {
1740                     imagejpeg($img, NULL, $quality);
1741                 }
1742                 break;
1744             case 'image/png':
1745                 $quality = (int)$quality;
1747                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1748                 // the latter of which can go to 100, we need to make sure that quality here is
1749                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1750                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1751                 imagepng($img, NULL, $quality, NULL);
1752                 break;
1754             default:
1755                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1756         }
1758         $content = ob_get_contents();
1759         ob_end_clean();
1760         imagedestroy($img);
1762         if (!$content) {
1763             throw new file_exception('storedfileproblem', 'Can not convert image');
1764         }
1766         return $this->create_file_from_string($filerecord, $content);
1767     }
1769     /**
1770      * Add file content to sha1 pool.
1771      *
1772      * @param string $pathname path to file
1773      * @param string $contenthash sha1 hash of content if known (performance only)
1774      * @return array (contenthash, filesize, newfile)
1775      */
1776     public function add_file_to_pool($pathname, $contenthash = NULL) {
1777         global $CFG;
1779         if (!is_readable($pathname)) {
1780             throw new file_exception('storedfilecannotread', '', $pathname);
1781         }
1783         $filesize = filesize($pathname);
1784         if ($filesize === false) {
1785             throw new file_exception('storedfilecannotread', '', $pathname);
1786         }
1788         if (is_null($contenthash)) {
1789             $contenthash = sha1_file($pathname);
1790         } else if ($CFG->debugdeveloper) {
1791             $filehash = sha1_file($pathname);
1792             if ($filehash === false) {
1793                 throw new file_exception('storedfilecannotread', '', $pathname);
1794             }
1795             if ($filehash !== $contenthash) {
1796                 // Hopefully this never happens, if yes we need to fix calling code.
1797                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1798                 $contenthash = $filehash;
1799             }
1800         }
1801         if ($contenthash === false) {
1802             throw new file_exception('storedfilecannotread', '', $pathname);
1803         }
1805         if ($filesize > 0 and $contenthash === sha1('')) {
1806             // Did the file change or is sha1_file() borked for this file?
1807             clearstatcache();
1808             $contenthash = sha1_file($pathname);
1809             $filesize = filesize($pathname);
1811             if ($contenthash === false or $filesize === false) {
1812                 throw new file_exception('storedfilecannotread', '', $pathname);
1813             }
1814             if ($filesize > 0 and $contenthash === sha1('')) {
1815                 // This is very weird...
1816                 throw new file_exception('storedfilecannotread', '', $pathname);
1817             }
1818         }
1820         $hashpath = $this->path_from_hash($contenthash);
1821         $hashfile = "$hashpath/$contenthash";
1823         $newfile = true;
1825         if (file_exists($hashfile)) {
1826             if (filesize($hashfile) === $filesize) {
1827                 return array($contenthash, $filesize, false);
1828             }
1829             if (sha1_file($hashfile) === $contenthash) {
1830                 // Jackpot! We have a sha1 collision.
1831                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1832                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1833                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1834                 throw new file_pool_content_exception($contenthash);
1835             }
1836             debugging("Replacing invalid content file $contenthash");
1837             unlink($hashfile);
1838             $newfile = false;
1839         }
1841         if (!is_dir($hashpath)) {
1842             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1843                 // Permission trouble.
1844                 throw new file_exception('storedfilecannotcreatefiledirs');
1845             }
1846         }
1848         // Let's try to prevent some race conditions.
1850         $prev = ignore_user_abort(true);
1851         @unlink($hashfile.'.tmp');
1852         if (!copy($pathname, $hashfile.'.tmp')) {
1853             // Borked permissions or out of disk space.
1854             ignore_user_abort($prev);
1855             throw new file_exception('storedfilecannotcreatefile');
1856         }
1857         if (filesize($hashfile.'.tmp') !== $filesize) {
1858             // This should not happen.
1859             unlink($hashfile.'.tmp');
1860             ignore_user_abort($prev);
1861             throw new file_exception('storedfilecannotcreatefile');
1862         }
1863         rename($hashfile.'.tmp', $hashfile);
1864         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1865         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1866         ignore_user_abort($prev);
1868         return array($contenthash, $filesize, $newfile);
1869     }
1871     /**
1872      * Add string content to sha1 pool.
1873      *
1874      * @param string $content file content - binary string
1875      * @return array (contenthash, filesize, newfile)
1876      */
1877     public function add_string_to_pool($content) {
1878         global $CFG;
1880         $contenthash = sha1($content);
1881         $filesize = strlen($content); // binary length
1883         $hashpath = $this->path_from_hash($contenthash);
1884         $hashfile = "$hashpath/$contenthash";
1886         $newfile = true;
1888         if (file_exists($hashfile)) {
1889             if (filesize($hashfile) === $filesize) {
1890                 return array($contenthash, $filesize, false);
1891             }
1892             if (sha1_file($hashfile) === $contenthash) {
1893                 // Jackpot! We have a sha1 collision.
1894                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1895                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
1896                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
1897                 throw new file_pool_content_exception($contenthash);
1898             }
1899             debugging("Replacing invalid content file $contenthash");
1900             unlink($hashfile);
1901             $newfile = false;
1902         }
1904         if (!is_dir($hashpath)) {
1905             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1906                 // Permission trouble.
1907                 throw new file_exception('storedfilecannotcreatefiledirs');
1908             }
1909         }
1911         // Hopefully this works around most potential race conditions.
1913         $prev = ignore_user_abort(true);
1915         if (!empty($CFG->preventfilelocking)) {
1916             $newsize = file_put_contents($hashfile.'.tmp', $content);
1917         } else {
1918             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
1919         }
1921         if ($newsize === false) {
1922             // Borked permissions most likely.
1923             ignore_user_abort($prev);
1924             throw new file_exception('storedfilecannotcreatefile');
1925         }
1926         if (filesize($hashfile.'.tmp') !== $filesize) {
1927             // Out of disk space?
1928             unlink($hashfile.'.tmp');
1929             ignore_user_abort($prev);
1930             throw new file_exception('storedfilecannotcreatefile');
1931         }
1932         rename($hashfile.'.tmp', $hashfile);
1933         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1934         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1935         ignore_user_abort($prev);
1937         return array($contenthash, $filesize, $newfile);
1938     }
1940     /**
1941      * Serve file content using X-Sendfile header.
1942      * Please make sure that all headers are already sent
1943      * and the all access control checks passed.
1944      *
1945      * @param string $contenthash sah1 hash of the file content to be served
1946      * @return bool success
1947      */
1948     public function xsendfile($contenthash) {
1949         global $CFG;
1950         require_once("$CFG->libdir/xsendfilelib.php");
1952         $hashpath = $this->path_from_hash($contenthash);
1953         return xsendfile("$hashpath/$contenthash");
1954     }
1956     /**
1957      * Content exists
1958      *
1959      * @param string $contenthash
1960      * @return bool
1961      */
1962     public function content_exists($contenthash) {
1963         $dir = $this->path_from_hash($contenthash);
1964         $filepath = $dir . '/' . $contenthash;
1965         return file_exists($filepath);
1966     }
1968     /**
1969      * Return path to file with given hash.
1970      *
1971      * NOTE: must not be public, files in pool must not be modified
1972      *
1973      * @param string $contenthash content hash
1974      * @return string expected file location
1975      */
1976     protected function path_from_hash($contenthash) {
1977         $l1 = $contenthash[0].$contenthash[1];
1978         $l2 = $contenthash[2].$contenthash[3];
1979         return "$this->filedir/$l1/$l2";
1980     }
1982     /**
1983      * Return path to file with given hash.
1984      *
1985      * NOTE: must not be public, files in pool must not be modified
1986      *
1987      * @param string $contenthash content hash
1988      * @return string expected file location
1989      */
1990     protected function trash_path_from_hash($contenthash) {
1991         $l1 = $contenthash[0].$contenthash[1];
1992         $l2 = $contenthash[2].$contenthash[3];
1993         return "$this->trashdir/$l1/$l2";
1994     }
1996     /**
1997      * Tries to recover missing content of file from trash.
1998      *
1999      * @param stored_file $file stored_file instance
2000      * @return bool success
2001      */
2002     public function try_content_recovery($file) {
2003         $contenthash = $file->get_contenthash();
2004         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
2005         if (!is_readable($trashfile)) {
2006             if (!is_readable($this->trashdir.'/'.$contenthash)) {
2007                 return false;
2008             }
2009             // nice, at least alternative trash file in trash root exists
2010             $trashfile = $this->trashdir.'/'.$contenthash;
2011         }
2012         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
2013             //weird, better fail early
2014             return false;
2015         }
2016         $contentdir  = $this->path_from_hash($contenthash);
2017         $contentfile = $contentdir.'/'.$contenthash;
2018         if (file_exists($contentfile)) {
2019             //strange, no need to recover anything
2020             return true;
2021         }
2022         if (!is_dir($contentdir)) {
2023             if (!mkdir($contentdir, $this->dirpermissions, true)) {
2024                 return false;
2025             }
2026         }
2027         return rename($trashfile, $contentfile);
2028     }
2030     /**
2031      * Marks pool file as candidate for deleting.
2032      *
2033      * DO NOT call directly - reserved for core!!
2034      *
2035      * @param string $contenthash
2036      */
2037     public function deleted_file_cleanup($contenthash) {
2038         global $DB;
2040         if ($contenthash === sha1('')) {
2041             // No need to delete empty content file with sha1('') content hash.
2042             return;
2043         }
2045         //Note: this section is critical - in theory file could be reused at the same
2046         //      time, if this happens we can still recover the file from trash
2047         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
2048             // file content is still used
2049             return;
2050         }
2051         //move content file to trash
2052         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
2053         if (!file_exists($contentfile)) {
2054             //weird, but no problem
2055             return;
2056         }
2057         $trashpath = $this->trash_path_from_hash($contenthash);
2058         $trashfile = $trashpath.'/'.$contenthash;
2059         if (file_exists($trashfile)) {
2060             // we already have this content in trash, no need to move it there
2061             unlink($contentfile);
2062             return;
2063         }
2064         if (!is_dir($trashpath)) {
2065             mkdir($trashpath, $this->dirpermissions, true);
2066         }
2067         rename($contentfile, $trashfile);
2068         chmod($trashfile, $this->filepermissions); // fix permissions if needed
2069     }
2071     /**
2072      * When user referring to a moodle file, we build the reference field
2073      *
2074      * @param array $params
2075      * @return string
2076      */
2077     public static function pack_reference($params) {
2078         $params = (array)$params;
2079         $reference = array();
2080         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
2081         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
2082         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
2083         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
2084         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
2085         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
2086         return base64_encode(serialize($reference));
2087     }
2089     /**
2090      * Unpack reference field
2091      *
2092      * @param string $str
2093      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
2094      * @throws file_reference_exception if the $str does not have the expected format
2095      * @return array
2096      */
2097     public static function unpack_reference($str, $cleanparams = false) {
2098         $decoded = base64_decode($str, true);
2099         if ($decoded === false) {
2100             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
2101         }
2102         $params = @unserialize($decoded); // hide E_NOTICE
2103         if ($params === false) {
2104             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
2105         }
2106         if (is_array($params) && $cleanparams) {
2107             $params = array(
2108                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
2109                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
2110                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
2111                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
2112                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
2113                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
2114             );
2115         }
2116         return $params;
2117     }
2119     /**
2120      * Search through the server files.
2121      *
2122      * The query parameter will be used in conjuction with the SQL directive
2123      * LIKE, so include '%' in it if you need to. This search will always ignore
2124      * user files and directories. Note that the search is case insensitive.
2125      *
2126      * This query can quickly become inefficient so use it sparignly.
2127      *
2128      * @param  string  $query The string used with SQL LIKE.
2129      * @param  integer $from  The offset to start the search at.
2130      * @param  integer $limit The maximum number of results.
2131      * @param  boolean $count When true this methods returns the number of results availabe,
2132      *                        disregarding the parameters $from and $limit.
2133      * @return int|array      Integer when count, otherwise array of stored_file objects.
2134      */
2135     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
2136         global $DB;
2137         $params = array(
2138             'contextlevel' => CONTEXT_USER,
2139             'directory' => '.',
2140             'query' => $query
2141         );
2143         if ($count) {
2144             $select = 'COUNT(1)';
2145         } else {
2146             $select = self::instance_sql_fields('f', 'r');
2147         }
2148         $like = $DB->sql_like('f.filename', ':query', false);
2150         $sql = "SELECT $select
2151                   FROM {files} f
2152              LEFT JOIN {files_reference} r
2153                     ON f.referencefileid = r.id
2154                   JOIN {context} c
2155                     ON f.contextid = c.id
2156                  WHERE c.contextlevel <> :contextlevel
2157                    AND f.filename <> :directory
2158                    AND " . $like . "";
2160         if ($count) {
2161             return $DB->count_records_sql($sql, $params);
2162         }
2164         $sql .= " ORDER BY f.filename";
2166         $result = array();
2167         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2168         foreach ($filerecords as $filerecord) {
2169             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2170         }
2171         $filerecords->close();
2173         return $result;
2174     }
2176     /**
2177      * Returns all aliases that refer to some stored_file via the given reference
2178      *
2179      * All repositories that provide access to a stored_file are expected to use
2180      * {@link self::pack_reference()}. This method can't be used if the given reference
2181      * does not use this format or if you are looking for references to an external file
2182      * (for example it can't be used to search for all aliases that refer to a given
2183      * Dropbox or Box.net file).
2184      *
2185      * Aliases in user draft areas are excluded from the returned list.
2186      *
2187      * @param string $reference identification of the referenced file
2188      * @return array of stored_file indexed by its pathnamehash
2189      */
2190     public function search_references($reference) {
2191         global $DB;
2193         if (is_null($reference)) {
2194             throw new coding_exception('NULL is not a valid reference to an external file');
2195         }
2197         // Give {@link self::unpack_reference()} a chance to throw exception if the
2198         // reference is not in a valid format.
2199         self::unpack_reference($reference);
2201         $referencehash = sha1($reference);
2203         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2204                   FROM {files} f
2205                   JOIN {files_reference} r ON f.referencefileid = r.id
2206                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2207                  WHERE r.referencehash = ?
2208                        AND (f.component <> ? OR f.filearea <> ?)";
2210         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2211         $files = array();
2212         foreach ($rs as $filerecord) {
2213             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2214         }
2216         return $files;
2217     }
2219     /**
2220      * Returns the number of aliases that refer to some stored_file via the given reference
2221      *
2222      * All repositories that provide access to a stored_file are expected to use
2223      * {@link self::pack_reference()}. This method can't be used if the given reference
2224      * does not use this format or if you are looking for references to an external file
2225      * (for example it can't be used to count aliases that refer to a given Dropbox or
2226      * Box.net file).
2227      *
2228      * Aliases in user draft areas are not counted.
2229      *
2230      * @param string $reference identification of the referenced file
2231      * @return int
2232      */
2233     public function search_references_count($reference) {
2234         global $DB;
2236         if (is_null($reference)) {
2237             throw new coding_exception('NULL is not a valid reference to an external file');
2238         }
2240         // Give {@link self::unpack_reference()} a chance to throw exception if the
2241         // reference is not in a valid format.
2242         self::unpack_reference($reference);
2244         $referencehash = sha1($reference);
2246         $sql = "SELECT COUNT(f.id)
2247                   FROM {files} f
2248                   JOIN {files_reference} r ON f.referencefileid = r.id
2249                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2250                  WHERE r.referencehash = ?
2251                        AND (f.component <> ? OR f.filearea <> ?)";
2253         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2254     }
2256     /**
2257      * Returns all aliases that link to the given stored_file
2258      *
2259      * Aliases in user draft areas are excluded from the returned list.
2260      *
2261      * @param stored_file $storedfile
2262      * @return array of stored_file
2263      */
2264     public function get_references_by_storedfile(stored_file $storedfile) {
2265         global $DB;
2267         $params = array();
2268         $params['contextid'] = $storedfile->get_contextid();
2269         $params['component'] = $storedfile->get_component();
2270         $params['filearea']  = $storedfile->get_filearea();
2271         $params['itemid']    = $storedfile->get_itemid();
2272         $params['filename']  = $storedfile->get_filename();
2273         $params['filepath']  = $storedfile->get_filepath();
2275         return $this->search_references(self::pack_reference($params));
2276     }
2278     /**
2279      * Returns the number of aliases that link to the given stored_file
2280      *
2281      * Aliases in user draft areas are not counted.
2282      *
2283      * @param stored_file $storedfile
2284      * @return int
2285      */
2286     public function get_references_count_by_storedfile(stored_file $storedfile) {
2287         global $DB;
2289         $params = array();
2290         $params['contextid'] = $storedfile->get_contextid();
2291         $params['component'] = $storedfile->get_component();
2292         $params['filearea']  = $storedfile->get_filearea();
2293         $params['itemid']    = $storedfile->get_itemid();
2294         $params['filename']  = $storedfile->get_filename();
2295         $params['filepath']  = $storedfile->get_filepath();
2297         return $this->search_references_count(self::pack_reference($params));
2298     }
2300     /**
2301      * Updates all files that are referencing this file with the new contenthash
2302      * and filesize
2303      *
2304      * @param stored_file $storedfile
2305      */
2306     public function update_references_to_storedfile(stored_file $storedfile) {
2307         global $CFG, $DB;
2308         $params = array();
2309         $params['contextid'] = $storedfile->get_contextid();
2310         $params['component'] = $storedfile->get_component();
2311         $params['filearea']  = $storedfile->get_filearea();
2312         $params['itemid']    = $storedfile->get_itemid();
2313         $params['filename']  = $storedfile->get_filename();
2314         $params['filepath']  = $storedfile->get_filepath();
2315         $reference = self::pack_reference($params);
2316         $referencehash = sha1($reference);
2318         $sql = "SELECT repositoryid, id FROM {files_reference}
2319                  WHERE referencehash = ?";
2320         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2322         $now = time();
2323         foreach ($rs as $record) {
2324             $this->update_references($record->id, $now, null,
2325                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2326         }
2327         $rs->close();
2328     }
2330     /**
2331      * Convert file alias to local file
2332      *
2333      * @throws moodle_exception if file could not be downloaded
2334      *
2335      * @param stored_file $storedfile a stored_file instances
2336      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2337      * @return stored_file stored_file
2338      */
2339     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2340         global $CFG;
2341         $storedfile->import_external_file_contents($maxbytes);
2342         $storedfile->delete_reference();
2343         return $storedfile;
2344     }
2346     /**
2347      * Return mimetype by given file pathname
2348      *
2349      * If file has a known extension, we return the mimetype based on extension.
2350      * Otherwise (when possible) we try to get the mimetype from file contents.
2351      *
2352      * @param string $pathname full path to the file
2353      * @param string $filename correct file name with extension, if omitted will be taken from $path
2354      * @return string
2355      */
2356     public static function mimetype($pathname, $filename = null) {
2357         if (empty($filename)) {
2358             $filename = $pathname;
2359         }
2360         $type = mimeinfo('type', $filename);
2361         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2362             $finfo = new finfo(FILEINFO_MIME_TYPE);
2363             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2364         }
2365         return $type;
2366     }
2368     /**
2369      * Cron cleanup job.
2370      */
2371     public function cron() {
2372         global $CFG, $DB;
2373         require_once($CFG->libdir.'/cronlib.php');
2375         // find out all stale draft areas (older than 4 days) and purge them
2376         // those are identified by time stamp of the /. root dir
2377         mtrace('Deleting old draft files... ', '');
2378         cron_trace_time_and_memory();
2379         $old = time() - 60*60*24*4;
2380         $sql = "SELECT *
2381                   FROM {files}
2382                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2383                        AND timecreated < :old";
2384         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2385         foreach ($rs as $dir) {
2386             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2387         }
2388         $rs->close();
2389         mtrace('done.');
2391         // remove orphaned preview files (that is files in the core preview filearea without
2392         // the existing original file)
2393         mtrace('Deleting orphaned preview files... ', '');
2394         cron_trace_time_and_memory();
2395         $sql = "SELECT p.*
2396                   FROM {files} p
2397              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2398                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2399                        AND o.id IS NULL";
2400         $syscontext = context_system::instance();
2401         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2402         foreach ($rs as $orphan) {
2403             $file = $this->get_file_instance($orphan);
2404             if (!$file->is_directory()) {
2405                 $file->delete();
2406             }
2407         }
2408         $rs->close();
2409         mtrace('done.');
2411         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2412         // the existing original file).
2413         mtrace('Deleting orphaned document conversion files... ', '');
2414         cron_trace_time_and_memory();
2415         $sql = "SELECT p.*
2416                   FROM {files} p
2417              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2418                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
2419                        AND o.id IS NULL";
2420         $syscontext = context_system::instance();
2421         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2422         foreach ($rs as $orphan) {
2423             $file = $this->get_file_instance($orphan);
2424             if (!$file->is_directory()) {
2425                 $file->delete();
2426             }
2427         }
2428         $rs->close();
2429         mtrace('done.');
2431         // remove trash pool files once a day
2432         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2433         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2434             require_once($CFG->libdir.'/filelib.php');
2435             // Delete files that are associated with a context that no longer exists.
2436             mtrace('Cleaning up files from deleted contexts... ', '');
2437             cron_trace_time_and_memory();
2438             $sql = "SELECT DISTINCT f.contextid
2439                     FROM {files} f
2440                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2441                     WHERE c.id IS NULL";
2442             $rs = $DB->get_recordset_sql($sql);
2443             if ($rs->valid()) {
2444                 $fs = get_file_storage();
2445                 foreach ($rs as $ctx) {
2446                     $fs->delete_area_files($ctx->contextid);
2447                 }
2448             }
2449             $rs->close();
2450             mtrace('done.');
2452             mtrace('Deleting trash files... ', '');
2453             cron_trace_time_and_memory();
2454             fulldelete($this->trashdir);
2455             set_config('fileslastcleanup', time());
2456             mtrace('done.');
2457         }
2458     }
2460     /**
2461      * Get the sql formated fields for a file instance to be created from a
2462      * {files} and {files_refernece} join.
2463      *
2464      * @param string $filesprefix the table prefix for the {files} table
2465      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2466      * @return string the sql to go after a SELECT
2467      */
2468     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2469         // Note, these fieldnames MUST NOT overlap between the two tables,
2470         // else problems like MDL-33172 occur.
2471         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2472             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2473             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2475         $referencefields = array('repositoryid' => 'repositoryid',
2476             'reference' => 'reference',
2477             'lastsync' => 'referencelastsync');
2479         // id is specifically named to prevent overlaping between the two tables.
2480         $fields = array();
2481         $fields[] = $filesprefix.'.id AS id';
2482         foreach ($filefields as $field) {
2483             $fields[] = "{$filesprefix}.{$field}";
2484         }
2486         foreach ($referencefields as $field => $alias) {
2487             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2488         }
2490         return implode(', ', $fields);
2491     }
2493     /**
2494      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2495      *
2496      * If the record already exists, its id is returned. If there is no such record yet,
2497      * new one is created (using the lastsync provided, too) and its id is returned.
2498      *
2499      * @param int $repositoryid
2500      * @param string $reference
2501      * @param int $lastsync
2502      * @param int $lifetime argument not used any more
2503      * @return int
2504      */
2505     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2506         global $DB;
2508         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2510         if ($id !== false) {
2511             // bah, that was easy
2512             return $id;
2513         }
2515         // no such record yet, create one
2516         try {
2517             $id = $DB->insert_record('files_reference', array(
2518                 'repositoryid'  => $repositoryid,
2519                 'reference'     => $reference,
2520                 'referencehash' => sha1($reference),
2521                 'lastsync'      => $lastsync));
2522         } catch (dml_exception $e) {
2523             // if inserting the new record failed, chances are that the race condition has just
2524             // occured and the unique index did not allow to create the second record with the same
2525             // repositoryid + reference combo
2526             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2527         }
2529         return $id;
2530     }
2532     /**
2533      * Returns the id of the record in {files_reference} that matches the passed parameters
2534      *
2535      * Depending on the required strictness, false can be returned. The behaviour is consistent
2536      * with standard DML methods.
2537      *
2538      * @param int $repositoryid
2539      * @param string $reference
2540      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2541      * @return int|bool
2542      */
2543     private function get_referencefileid($repositoryid, $reference, $strictness) {
2544         global $DB;
2546         return $DB->get_field('files_reference', 'id',
2547             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2548     }
2550     /**
2551      * Updates a reference to the external resource and all files that use it
2552      *
2553      * This function is called after synchronisation of an external file and updates the
2554      * contenthash, filesize and status of all files that reference this external file
2555      * as well as time last synchronised.
2556      *
2557      * @param int $referencefileid
2558      * @param int $lastsync
2559      * @param int $lifetime argument not used any more, liefetime is returned by repository
2560      * @param string $contenthash
2561      * @param int $filesize
2562      * @param int $status 0 if ok or 666 if source is missing
2563      * @param int $timemodified last time modified of the source, if known
2564      */
2565     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2566         global $DB;
2567         $referencefileid = clean_param($referencefileid, PARAM_INT);
2568         $lastsync = clean_param($lastsync, PARAM_INT);
2569         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2570         $filesize = clean_param($filesize, PARAM_INT);
2571         $status = clean_param($status, PARAM_INT);
2572         $params = array('contenthash' => $contenthash,
2573                     'filesize' => $filesize,
2574                     'status' => $status,
2575                     'referencefileid' => $referencefileid,
2576                     'timemodified' => $timemodified);
2577         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2578             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2579             WHERE referencefileid = :referencefileid', $params);
2580         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2581         $DB->update_record('files_reference', (object)$data);
2582     }