917705b449334b002dad9022237114dfa0f70eac
[moodle.git] / lib / filestorage / file_storage.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Core file storage class definition.
20  *
21  * @package   core_files
22  * @copyright 2008 Petr Skoda {@link http://skodak.org}
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/stored_file.php");
30 /**
31  * File storage class used for low level access to stored files.
32  *
33  * Only owner of file area may use this class to access own files,
34  * for example only code in mod/assignment/* may access assignment
35  * attachments. When some other part of moodle needs to access
36  * files of modules it has to use file_browser class instead or there
37  * has to be some callback API.
38  *
39  * @package   core_files
40  * @category  files
41  * @copyright 2008 Petr Skoda {@link http://skodak.org}
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  * @since     Moodle 2.0
44  */
45 class file_storage {
46     /** @var string Directory with file contents */
47     private $filedir;
48     /** @var string Contents of deleted files not needed any more */
49     private $trashdir;
50     /** @var string tempdir */
51     private $tempdir;
52     /** @var int Permissions for new directories */
53     private $dirpermissions;
54     /** @var int Permissions for new files */
55     private $filepermissions;
57     /**
58      * Constructor - do not use directly use {@link get_file_storage()} call instead.
59      *
60      * @param string $filedir full path to pool directory
61      * @param string $trashdir temporary storage of deleted area
62      * @param string $tempdir temporary storage of various files
63      * @param int $dirpermissions new directory permissions
64      * @param int $filepermissions new file permissions
65      */
66     public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
67         $this->filedir         = $filedir;
68         $this->trashdir        = $trashdir;
69         $this->tempdir         = $tempdir;
70         $this->dirpermissions  = $dirpermissions;
71         $this->filepermissions = $filepermissions;
73         // make sure the file pool directory exists
74         if (!is_dir($this->filedir)) {
75             if (!mkdir($this->filedir, $this->dirpermissions, true)) {
76                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
77             }
78             // place warning file in file pool root
79             if (!file_exists($this->filedir.'/warning.txt')) {
80                 file_put_contents($this->filedir.'/warning.txt',
81                                   '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.');
82             }
83         }
84         // make sure the file pool directory exists
85         if (!is_dir($this->trashdir)) {
86             if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
87                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
88             }
89         }
90     }
92     /**
93      * Calculates sha1 hash of unique full path name information.
94      *
95      * This hash is a unique file identifier - it is used to improve
96      * performance and overcome db index size limits.
97      *
98      * @param int $contextid context ID
99      * @param string $component component
100      * @param string $filearea file area
101      * @param int $itemid item ID
102      * @param string $filepath file path
103      * @param string $filename file name
104      * @return string sha1 hash
105      */
106     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
108     }
110     /**
111      * Does this file exist?
112      *
113      * @param int $contextid context ID
114      * @param string $component component
115      * @param string $filearea file area
116      * @param int $itemid item ID
117      * @param string $filepath file path
118      * @param string $filename file name
119      * @return bool
120      */
121     public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
122         $filepath = clean_param($filepath, PARAM_PATH);
123         $filename = clean_param($filename, PARAM_FILE);
125         if ($filename === '') {
126             $filename = '.';
127         }
129         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
130         return $this->file_exists_by_hash($pathnamehash);
131     }
133     /**
134      * Whether or not the file exist
135      *
136      * @param string $pathnamehash path name hash
137      * @return bool
138      */
139     public function file_exists_by_hash($pathnamehash) {
140         global $DB;
142         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
143     }
145     /**
146      * Create instance of file class from database record.
147      *
148      * @param stdClass $filerecord record from the files table left join files_reference table
149      * @return stored_file instance of file abstraction class
150      */
151     public function get_file_instance(stdClass $filerecord) {
152         $storedfile = new stored_file($this, $filerecord, $this->filedir);
153         return $storedfile;
154     }
156     /**
157      * Returns an image file that represent the given stored file as a preview
158      *
159      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
160      * future, the support for other mimetypes can be added, too (eg. generate an image
161      * preview of PDF, text documents etc).
162      *
163      * @param stored_file $file the file we want to preview
164      * @param string $mode preview mode, eg. 'thumb'
165      * @return stored_file|bool false if unable to create the preview, stored file otherwise
166      */
167     public function get_file_preview(stored_file $file, $mode) {
169         $context = context_system::instance();
170         $path = '/' . trim($mode, '/') . '/';
171         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
173         if (!$preview) {
174             $preview = $this->create_file_preview($file, $mode);
175             if (!$preview) {
176                 return false;
177             }
178         }
180         return $preview;
181     }
183     /**
184      * Generates a preview image for the stored file
185      *
186      * @param stored_file $file the file we want to preview
187      * @param string $mode preview mode, eg. 'thumb'
188      * @return stored_file|bool the newly created preview file or false
189      */
190     protected function create_file_preview(stored_file $file, $mode) {
192         $mimetype = $file->get_mimetype();
194         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
195             // make a preview of the image
196             $data = $this->create_imagefile_preview($file, $mode);
198         } else {
199             // unable to create the preview of this mimetype yet
200             return false;
201         }
203         if (empty($data)) {
204             return false;
205         }
207         // getimagesizefromstring() is available from PHP 5.4 but we need to support
208         // lower versions, so...
209         $tmproot = make_temp_directory('thumbnails');
210         $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
211         file_put_contents($tmpfilepath, $data);
212         $imageinfo = getimagesize($tmpfilepath);
213         unlink($tmpfilepath);
215         $context = context_system::instance();
217         $record = array(
218             'contextid' => $context->id,
219             'component' => 'core',
220             'filearea'  => 'preview',
221             'itemid'    => 0,
222             'filepath'  => '/' . trim($mode, '/') . '/',
223             'filename'  => $file->get_contenthash(),
224         );
226         if ($imageinfo) {
227             $record['mimetype'] = $imageinfo['mime'];
228         }
230         return $this->create_file_from_string($record, $data);
231     }
233     /**
234      * Generates a preview for the stored image file
235      *
236      * @param stored_file $file the image we want to preview
237      * @param string $mode preview mode, eg. 'thumb'
238      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
239      */
240     protected function create_imagefile_preview(stored_file $file, $mode) {
241         global $CFG;
242         require_once($CFG->libdir.'/gdlib.php');
244         $tmproot = make_temp_directory('thumbnails');
245         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
246         $file->copy_content_to($tmpfilepath);
248         if ($mode === 'tinyicon') {
249             $data = generate_image_thumbnail($tmpfilepath, 24, 24);
251         } else if ($mode === 'thumb') {
252             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
254         } else {
255             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
256         }
258         unlink($tmpfilepath);
260         return $data;
261     }
263     /**
264      * Fetch file using local file id.
265      *
266      * Please do not rely on file ids, it is usually easier to use
267      * pathname hashes instead.
268      *
269      * @param int $fileid file ID
270      * @return stored_file|bool stored_file instance if exists, false if not
271      */
272     public function get_file_by_id($fileid) {
273         global $DB;
275         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
276                   FROM {files} f
277              LEFT JOIN {files_reference} r
278                        ON f.referencefileid = r.id
279                  WHERE f.id = ?";
280         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
281             return $this->get_file_instance($filerecord);
282         } else {
283             return false;
284         }
285     }
287     /**
288      * Fetch file using local file full pathname hash
289      *
290      * @param string $pathnamehash path name hash
291      * @return stored_file|bool stored_file instance if exists, false if not
292      */
293     public function get_file_by_hash($pathnamehash) {
294         global $DB;
296         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
297                   FROM {files} f
298              LEFT JOIN {files_reference} r
299                        ON f.referencefileid = r.id
300                  WHERE f.pathnamehash = ?";
301         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
302             return $this->get_file_instance($filerecord);
303         } else {
304             return false;
305         }
306     }
308     /**
309      * Fetch locally stored file.
310      *
311      * @param int $contextid context ID
312      * @param string $component component
313      * @param string $filearea file area
314      * @param int $itemid item ID
315      * @param string $filepath file path
316      * @param string $filename file name
317      * @return stored_file|bool stored_file instance if exists, false if not
318      */
319     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
320         $filepath = clean_param($filepath, PARAM_PATH);
321         $filename = clean_param($filename, PARAM_FILE);
323         if ($filename === '') {
324             $filename = '.';
325         }
327         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
328         return $this->get_file_by_hash($pathnamehash);
329     }
331     /**
332      * Are there any files (or directories)
333      *
334      * @param int $contextid context ID
335      * @param string $component component
336      * @param string $filearea file area
337      * @param bool|int $itemid item id or false if all items
338      * @param bool $ignoredirs whether or not ignore directories
339      * @return bool empty
340      */
341     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
342         global $DB;
344         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
345         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
347         if ($itemid !== false) {
348             $params['itemid'] = $itemid;
349             $where .= " AND itemid = :itemid";
350         }
352         if ($ignoredirs) {
353             $sql = "SELECT 'x'
354                       FROM {files}
355                      WHERE $where AND filename <> '.'";
356         } else {
357             $sql = "SELECT 'x'
358                       FROM {files}
359                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
360         }
362         return !$DB->record_exists_sql($sql, $params);
363     }
365     /**
366      * Returns all files belonging to given repository
367      *
368      * @param int $repositoryid
369      * @param string $sort A fragment of SQL to use for sorting
370      */
371     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
372         global $DB;
373         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
374                   FROM {files} f
375              LEFT JOIN {files_reference} r
376                        ON f.referencefileid = r.id
377                  WHERE r.repositoryid = ?";
378         if (!empty($sort)) {
379             $sql .= " ORDER BY {$sort}";
380         }
382         $result = array();
383         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
384         foreach ($filerecords as $filerecord) {
385             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
386         }
387         return $result;
388     }
390     /**
391      * Returns all area files (optionally limited by itemid)
392      *
393      * @param int $contextid context ID
394      * @param string $component component
395      * @param string $filearea file area
396      * @param int $itemid item ID or all files if not specified
397      * @param string $sort A fragment of SQL to use for sorting
398      * @param bool $includedirs whether or not include directories
399      * @return array of stored_files indexed by pathanmehash
400      */
401     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "sortorder, itemid, filepath, filename", $includedirs = true) {
402         global $DB;
404         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
405         if ($itemid !== false) {
406             $itemidsql = ' AND f.itemid = :itemid ';
407             $conditions['itemid'] = $itemid;
408         } else {
409             $itemidsql = '';
410         }
412         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
413                   FROM {files} f
414              LEFT JOIN {files_reference} r
415                        ON f.referencefileid = r.id
416                  WHERE f.contextid = :contextid
417                        AND f.component = :component
418                        AND f.filearea = :filearea
419                        $itemidsql";
420         if (!empty($sort)) {
421             $sql .= " ORDER BY {$sort}";
422         }
424         $result = array();
425         $filerecords = $DB->get_records_sql($sql, $conditions);
426         foreach ($filerecords as $filerecord) {
427             if (!$includedirs and $filerecord->filename === '.') {
428                 continue;
429             }
430             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
431         }
432         return $result;
433     }
435     /**
436      * Returns array based tree structure of area files
437      *
438      * @param int $contextid context ID
439      * @param string $component component
440      * @param string $filearea file area
441      * @param int $itemid item ID
442      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
443      */
444     public function get_area_tree($contextid, $component, $filearea, $itemid) {
445         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
446         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
447         // first create directory structure
448         foreach ($files as $hash=>$dir) {
449             if (!$dir->is_directory()) {
450                 continue;
451             }
452             unset($files[$hash]);
453             if ($dir->get_filepath() === '/') {
454                 $result['dirfile'] = $dir;
455                 continue;
456             }
457             $parts = explode('/', trim($dir->get_filepath(),'/'));
458             $pointer =& $result;
459             foreach ($parts as $part) {
460                 if ($part === '') {
461                     continue;
462                 }
463                 if (!isset($pointer['subdirs'][$part])) {
464                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
465                 }
466                 $pointer =& $pointer['subdirs'][$part];
467             }
468             $pointer['dirfile'] = $dir;
469             unset($pointer);
470         }
471         foreach ($files as $hash=>$file) {
472             $parts = explode('/', trim($file->get_filepath(),'/'));
473             $pointer =& $result;
474             foreach ($parts as $part) {
475                 if ($part === '') {
476                     continue;
477                 }
478                 $pointer =& $pointer['subdirs'][$part];
479             }
480             $pointer['files'][$file->get_filename()] = $file;
481             unset($pointer);
482         }
483         return $result;
484     }
486     /**
487      * Returns all files and optionally directories
488      *
489      * @param int $contextid context ID
490      * @param string $component component
491      * @param string $filearea file area
492      * @param int $itemid item ID
493      * @param int $filepath directory path
494      * @param bool $recursive include all subdirectories
495      * @param bool $includedirs include files and directories
496      * @param string $sort A fragment of SQL to use for sorting
497      * @return array of stored_files indexed by pathanmehash
498      */
499     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
500         global $DB;
502         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
503             return array();
504         }
506         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
508         if ($recursive) {
510             $dirs = $includedirs ? "" : "AND filename <> '.'";
511             $length = textlib::strlen($filepath);
513             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
514                       FROM {files} f
515                  LEFT JOIN {files_reference} r
516                            ON f.referencefileid = r.id
517                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
518                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
519                            AND f.id <> :dirid
520                            $dirs
521                            $orderby";
522             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
524             $files = array();
525             $dirs  = array();
526             $filerecords = $DB->get_records_sql($sql, $params);
527             foreach ($filerecords as $filerecord) {
528                 if ($filerecord->filename == '.') {
529                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
530                 } else {
531                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
532                 }
533             }
534             $result = array_merge($dirs, $files);
536         } else {
537             $result = array();
538             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
540             $length = textlib::strlen($filepath);
542             if ($includedirs) {
543                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
544                           FROM {files} f
545                      LEFT JOIN {files_reference} r
546                                ON f.referencefileid = r.id
547                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
548                                AND f.itemid = :itemid AND f.filename = '.'
549                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
550                                AND f.id <> :dirid
551                                $orderby";
552                 $reqlevel = substr_count($filepath, '/') + 1;
553                 $filerecords = $DB->get_records_sql($sql, $params);
554                 foreach ($filerecords as $filerecord) {
555                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
556                         continue;
557                     }
558                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
559                 }
560             }
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.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
567                            AND f.filepath = :filepath AND f.filename <> '.'
568                            $orderby";
570             $filerecords = $DB->get_records_sql($sql, $params);
571             foreach ($filerecords as $filerecord) {
572                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
573             }
574         }
576         return $result;
577     }
579     /**
580      * Delete all area files (optionally limited by itemid).
581      *
582      * @param int $contextid context ID
583      * @param string $component component
584      * @param string $filearea file area or all areas in context if not specified
585      * @param int $itemid item ID or all files if not specified
586      * @return bool success
587      */
588     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
589         global $DB;
591         $conditions = array('contextid'=>$contextid);
592         if ($component !== false) {
593             $conditions['component'] = $component;
594         }
595         if ($filearea !== false) {
596             $conditions['filearea'] = $filearea;
597         }
598         if ($itemid !== false) {
599             $conditions['itemid'] = $itemid;
600         }
602         $filerecords = $DB->get_records('files', $conditions);
603         foreach ($filerecords as $filerecord) {
604             $this->get_file_instance($filerecord)->delete();
605         }
607         return true; // BC only
608     }
610     /**
611      * Delete all the files from certain areas where itemid is limited by an
612      * arbitrary bit of SQL.
613      *
614      * @param int $contextid the id of the context the files belong to. Must be given.
615      * @param string $component the owning component. Must be given.
616      * @param string $filearea the file area name. Must be given.
617      * @param string $itemidstest an SQL fragment that the itemid must match. Used
618      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
619      *      and may not used named parameters called contextid, component or filearea.
620      * @param array $params any query params used by $itemidstest.
621      */
622     public function delete_area_files_select($contextid, $component,
623             $filearea, $itemidstest, array $params = null) {
624         global $DB;
626         $where = "contextid = :contextid
627                 AND component = :component
628                 AND filearea = :filearea
629                 AND itemid $itemidstest";
630         $params['contextid'] = $contextid;
631         $params['component'] = $component;
632         $params['filearea'] = $filearea;
634         $filerecords = $DB->get_recordset_select('files', $where, $params);
635         foreach ($filerecords as $filerecord) {
636             $this->get_file_instance($filerecord)->delete();
637         }
638         $filerecords->close();
639     }
641     /**
642      * Move all the files in a file area from one context to another.
643      *
644      * @param int $oldcontextid the context the files are being moved from.
645      * @param int $newcontextid the context the files are being moved to.
646      * @param string $component the plugin that these files belong to.
647      * @param string $filearea the name of the file area.
648      * @param int $itemid file item ID
649      * @return int the number of files moved, for information.
650      */
651     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
652         // Note, this code is based on some code that Petr wrote in
653         // forum_move_attachments in mod/forum/lib.php. I moved it here because
654         // I needed it in the question code too.
655         $count = 0;
657         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
658         foreach ($oldfiles as $oldfile) {
659             $filerecord = new stdClass();
660             $filerecord->contextid = $newcontextid;
661             $this->create_file_from_storedfile($filerecord, $oldfile);
662             $count += 1;
663         }
665         if ($count) {
666             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
667         }
669         return $count;
670     }
672     /**
673      * Recursively creates directory.
674      *
675      * @param int $contextid context ID
676      * @param string $component component
677      * @param string $filearea file area
678      * @param int $itemid item ID
679      * @param string $filepath file path
680      * @param int $userid the user ID
681      * @return bool success
682      */
683     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
684         global $DB;
686         // validate all parameters, we do not want any rubbish stored in database, right?
687         if (!is_number($contextid) or $contextid < 1) {
688             throw new file_exception('storedfileproblem', 'Invalid contextid');
689         }
691         $component = clean_param($component, PARAM_COMPONENT);
692         if (empty($component)) {
693             throw new file_exception('storedfileproblem', 'Invalid component');
694         }
696         $filearea = clean_param($filearea, PARAM_AREA);
697         if (empty($filearea)) {
698             throw new file_exception('storedfileproblem', 'Invalid filearea');
699         }
701         if (!is_number($itemid) or $itemid < 0) {
702             throw new file_exception('storedfileproblem', 'Invalid itemid');
703         }
705         $filepath = clean_param($filepath, PARAM_PATH);
706         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
707             // path must start and end with '/'
708             throw new file_exception('storedfileproblem', 'Invalid file path');
709         }
711         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
713         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
714             return $dir_info;
715         }
717         static $contenthash = null;
718         if (!$contenthash) {
719             $this->add_string_to_pool('');
720             $contenthash = sha1('');
721         }
723         $now = time();
725         $dir_record = new stdClass();
726         $dir_record->contextid = $contextid;
727         $dir_record->component = $component;
728         $dir_record->filearea  = $filearea;
729         $dir_record->itemid    = $itemid;
730         $dir_record->filepath  = $filepath;
731         $dir_record->filename  = '.';
732         $dir_record->contenthash  = $contenthash;
733         $dir_record->filesize  = 0;
735         $dir_record->timecreated  = $now;
736         $dir_record->timemodified = $now;
737         $dir_record->mimetype     = null;
738         $dir_record->userid       = $userid;
740         $dir_record->pathnamehash = $pathnamehash;
742         $DB->insert_record('files', $dir_record);
743         $dir_info = $this->get_file_by_hash($pathnamehash);
745         if ($filepath !== '/') {
746             //recurse to parent dirs
747             $filepath = trim($filepath, '/');
748             $filepath = explode('/', $filepath);
749             array_pop($filepath);
750             $filepath = implode('/', $filepath);
751             $filepath = ($filepath === '') ? '/' : "/$filepath/";
752             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
753         }
755         return $dir_info;
756     }
758     /**
759      * Add new local file based on existing local file.
760      *
761      * @param stdClass|array $filerecord object or array describing changes
762      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
763      * @return stored_file instance of newly created file
764      */
765     public function create_file_from_storedfile($filerecord, $fileorid) {
766         global $DB;
768         if ($fileorid instanceof stored_file) {
769             $fid = $fileorid->get_id();
770         } else {
771             $fid = $fileorid;
772         }
774         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
776         unset($filerecord['id']);
777         unset($filerecord['filesize']);
778         unset($filerecord['contenthash']);
779         unset($filerecord['pathnamehash']);
781         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
782                   FROM {files} f
783              LEFT JOIN {files_reference} r
784                        ON f.referencefileid = r.id
785                  WHERE f.id = ?";
787         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
788             throw new file_exception('storedfileproblem', 'File does not exist');
789         }
791         unset($newrecord->id);
793         foreach ($filerecord as $key => $value) {
794             // validate all parameters, we do not want any rubbish stored in database, right?
795             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
796                 throw new file_exception('storedfileproblem', 'Invalid contextid');
797             }
799             if ($key == 'component') {
800                 $value = clean_param($value, PARAM_COMPONENT);
801                 if (empty($value)) {
802                     throw new file_exception('storedfileproblem', 'Invalid component');
803                 }
804             }
806             if ($key == 'filearea') {
807                 $value = clean_param($value, PARAM_AREA);
808                 if (empty($value)) {
809                     throw new file_exception('storedfileproblem', 'Invalid filearea');
810                 }
811             }
813             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
814                 throw new file_exception('storedfileproblem', 'Invalid itemid');
815             }
818             if ($key == 'filepath') {
819                 $value = clean_param($value, PARAM_PATH);
820                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
821                     // path must start and end with '/'
822                     throw new file_exception('storedfileproblem', 'Invalid file path');
823                 }
824             }
826             if ($key == 'filename') {
827                 $value = clean_param($value, PARAM_FILE);
828                 if ($value === '') {
829                     // path must start and end with '/'
830                     throw new file_exception('storedfileproblem', 'Invalid file name');
831                 }
832             }
834             if ($key === 'timecreated' or $key === 'timemodified') {
835                 if (!is_number($value)) {
836                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
837                 }
838                 if ($value < 0) {
839                     //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)
840                     $value = 0;
841                 }
842             }
844             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
845                 $value = clean_param($value, PARAM_INT);
846             }
848             $newrecord->$key = $value;
849         }
851         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
853         if ($newrecord->filename === '.') {
854             // special case - only this function supports directories ;-)
855             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
856             // update the existing directory with the new data
857             $newrecord->id = $directory->get_id();
858             $DB->update_record('files', $newrecord);
859             return $this->get_file_instance($newrecord);
860         }
862         // note: referencefileid is copied from the original file so that
863         // creating a new file from an existing alias creates new alias implicitly.
864         // here we just check the database consistency.
865         if (!empty($newrecord->repositoryid)) {
866             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
867                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
868             }
869         }
871         try {
872             $newrecord->id = $DB->insert_record('files', $newrecord);
873         } catch (dml_exception $e) {
874             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
875                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
876         }
879         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
881         return $this->get_file_instance($newrecord);
882     }
884     /**
885      * Add new local file.
886      *
887      * @param stdClass|array $filerecord object or array describing file
888      * @param string $url the URL to the file
889      * @param array $options {@link download_file_content()} options
890      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
891      * @return stored_file
892      */
893     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
895         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
896         $filerecord = (object)$filerecord; // We support arrays too.
898         $headers        = isset($options['headers'])        ? $options['headers'] : null;
899         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
900         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
901         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
902         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
903         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
904         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
906         if (!isset($filerecord->filename)) {
907             $parts = explode('/', $url);
908             $filename = array_pop($parts);
909             $filerecord->filename = clean_param($filename, PARAM_FILE);
910         }
911         $source = !empty($filerecord->source) ? $filerecord->source : $url;
912         $filerecord->source = clean_param($source, PARAM_URL);
914         if ($usetempfile) {
915             check_dir_exists($this->tempdir);
916             $tmpfile = tempnam($this->tempdir, 'newfromurl');
917             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
918             if ($content === false) {
919                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
920             }
921             try {
922                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
923                 @unlink($tmpfile);
924                 return $newfile;
925             } catch (Exception $e) {
926                 @unlink($tmpfile);
927                 throw $e;
928             }
930         } else {
931             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
932             if ($content === false) {
933                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
934             }
935             return $this->create_file_from_string($filerecord, $content);
936         }
937     }
939     /**
940      * Add new local file.
941      *
942      * @param stdClass|array $filerecord object or array describing file
943      * @param string $pathname path to file or content of file
944      * @return stored_file
945      */
946     public function create_file_from_pathname($filerecord, $pathname) {
947         global $DB;
949         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
950         $filerecord = (object)$filerecord; // We support arrays too.
952         // validate all parameters, we do not want any rubbish stored in database, right?
953         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
954             throw new file_exception('storedfileproblem', 'Invalid contextid');
955         }
957         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
958         if (empty($filerecord->component)) {
959             throw new file_exception('storedfileproblem', 'Invalid component');
960         }
962         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
963         if (empty($filerecord->filearea)) {
964             throw new file_exception('storedfileproblem', 'Invalid filearea');
965         }
967         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
968             throw new file_exception('storedfileproblem', 'Invalid itemid');
969         }
971         if (!empty($filerecord->sortorder)) {
972             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
973                 $filerecord->sortorder = 0;
974             }
975         } else {
976             $filerecord->sortorder = 0;
977         }
979         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
980         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
981             // path must start and end with '/'
982             throw new file_exception('storedfileproblem', 'Invalid file path');
983         }
985         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
986         if ($filerecord->filename === '') {
987             // filename must not be empty
988             throw new file_exception('storedfileproblem', 'Invalid file name');
989         }
991         $now = time();
992         if (isset($filerecord->timecreated)) {
993             if (!is_number($filerecord->timecreated)) {
994                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
995             }
996             if ($filerecord->timecreated < 0) {
997                 //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)
998                 $filerecord->timecreated = 0;
999             }
1000         } else {
1001             $filerecord->timecreated = $now;
1002         }
1004         if (isset($filerecord->timemodified)) {
1005             if (!is_number($filerecord->timemodified)) {
1006                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1007             }
1008             if ($filerecord->timemodified < 0) {
1009                 //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)
1010                 $filerecord->timemodified = 0;
1011             }
1012         } else {
1013             $filerecord->timemodified = $now;
1014         }
1016         $newrecord = new stdClass();
1018         $newrecord->contextid = $filerecord->contextid;
1019         $newrecord->component = $filerecord->component;
1020         $newrecord->filearea  = $filerecord->filearea;
1021         $newrecord->itemid    = $filerecord->itemid;
1022         $newrecord->filepath  = $filerecord->filepath;
1023         $newrecord->filename  = $filerecord->filename;
1025         $newrecord->timecreated  = $filerecord->timecreated;
1026         $newrecord->timemodified = $filerecord->timemodified;
1027         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1028         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1029         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1030         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1031         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1032         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1033         $newrecord->sortorder    = $filerecord->sortorder;
1035         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1037         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1039         try {
1040             $newrecord->id = $DB->insert_record('files', $newrecord);
1041         } catch (dml_exception $e) {
1042             if ($newfile) {
1043                 $this->deleted_file_cleanup($newrecord->contenthash);
1044             }
1045             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1046                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1047         }
1049         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1051         return $this->get_file_instance($newrecord);
1052     }
1054     /**
1055      * Add new local file.
1056      *
1057      * @param stdClass|array $filerecord object or array describing file
1058      * @param string $content content of file
1059      * @return stored_file
1060      */
1061     public function create_file_from_string($filerecord, $content) {
1062         global $DB;
1064         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1065         $filerecord = (object)$filerecord; // We support arrays too.
1067         // validate all parameters, we do not want any rubbish stored in database, right?
1068         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1069             throw new file_exception('storedfileproblem', 'Invalid contextid');
1070         }
1072         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1073         if (empty($filerecord->component)) {
1074             throw new file_exception('storedfileproblem', 'Invalid component');
1075         }
1077         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1078         if (empty($filerecord->filearea)) {
1079             throw new file_exception('storedfileproblem', 'Invalid filearea');
1080         }
1082         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1083             throw new file_exception('storedfileproblem', 'Invalid itemid');
1084         }
1086         if (!empty($filerecord->sortorder)) {
1087             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1088                 $filerecord->sortorder = 0;
1089             }
1090         } else {
1091             $filerecord->sortorder = 0;
1092         }
1094         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1095         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1096             // path must start and end with '/'
1097             throw new file_exception('storedfileproblem', 'Invalid file path');
1098         }
1100         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1101         if ($filerecord->filename === '') {
1102             // path must start and end with '/'
1103             throw new file_exception('storedfileproblem', 'Invalid file name');
1104         }
1106         $now = time();
1107         if (isset($filerecord->timecreated)) {
1108             if (!is_number($filerecord->timecreated)) {
1109                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1110             }
1111             if ($filerecord->timecreated < 0) {
1112                 //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)
1113                 $filerecord->timecreated = 0;
1114             }
1115         } else {
1116             $filerecord->timecreated = $now;
1117         }
1119         if (isset($filerecord->timemodified)) {
1120             if (!is_number($filerecord->timemodified)) {
1121                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1122             }
1123             if ($filerecord->timemodified < 0) {
1124                 //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)
1125                 $filerecord->timemodified = 0;
1126             }
1127         } else {
1128             $filerecord->timemodified = $now;
1129         }
1131         $newrecord = new stdClass();
1133         $newrecord->contextid = $filerecord->contextid;
1134         $newrecord->component = $filerecord->component;
1135         $newrecord->filearea  = $filerecord->filearea;
1136         $newrecord->itemid    = $filerecord->itemid;
1137         $newrecord->filepath  = $filerecord->filepath;
1138         $newrecord->filename  = $filerecord->filename;
1140         $newrecord->timecreated  = $filerecord->timecreated;
1141         $newrecord->timemodified = $filerecord->timemodified;
1142         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1143         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1144         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1145         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1146         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1147         $newrecord->sortorder    = $filerecord->sortorder;
1149         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1150         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1151         // get mimetype by magic bytes
1152         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1154         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1156         try {
1157             $newrecord->id = $DB->insert_record('files', $newrecord);
1158         } catch (dml_exception $e) {
1159             if ($newfile) {
1160                 $this->deleted_file_cleanup($newrecord->contenthash);
1161             }
1162             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1163                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1164         }
1166         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1168         return $this->get_file_instance($newrecord);
1169     }
1171     /**
1172      * Create a new alias/shortcut file from file reference information
1173      *
1174      * @param stdClass|array $filerecord object or array describing the new file
1175      * @param int $repositoryid the id of the repository that provides the original file
1176      * @param string $reference the information required by the repository to locate the original file
1177      * @param array $options options for creating the new file
1178      * @return stored_file
1179      */
1180     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1181         global $DB;
1183         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1184         $filerecord = (object)$filerecord; // We support arrays too.
1186         // validate all parameters, we do not want any rubbish stored in database, right?
1187         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1188             throw new file_exception('storedfileproblem', 'Invalid contextid');
1189         }
1191         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1192         if (empty($filerecord->component)) {
1193             throw new file_exception('storedfileproblem', 'Invalid component');
1194         }
1196         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1197         if (empty($filerecord->filearea)) {
1198             throw new file_exception('storedfileproblem', 'Invalid filearea');
1199         }
1201         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1202             throw new file_exception('storedfileproblem', 'Invalid itemid');
1203         }
1205         if (!empty($filerecord->sortorder)) {
1206             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1207                 $filerecord->sortorder = 0;
1208             }
1209         } else {
1210             $filerecord->sortorder = 0;
1211         }
1213         // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
1214         unset($filerecord->referencelastsync);
1215         unset($filerecord->referencelifetime);
1217         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1218         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1219         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1220         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1221         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1222         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1223         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1224         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1225             // Path must start and end with '/'.
1226             throw new file_exception('storedfileproblem', 'Invalid file path');
1227         }
1229         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1230         if ($filerecord->filename === '') {
1231             // Path must start and end with '/'.
1232             throw new file_exception('storedfileproblem', 'Invalid file name');
1233         }
1235         $now = time();
1236         if (isset($filerecord->timecreated)) {
1237             if (!is_number($filerecord->timecreated)) {
1238                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1239             }
1240             if ($filerecord->timecreated < 0) {
1241                 // 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)
1242                 $filerecord->timecreated = 0;
1243             }
1244         } else {
1245             $filerecord->timecreated = $now;
1246         }
1248         if (isset($filerecord->timemodified)) {
1249             if (!is_number($filerecord->timemodified)) {
1250                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1251             }
1252             if ($filerecord->timemodified < 0) {
1253                 // 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)
1254                 $filerecord->timemodified = 0;
1255             }
1256         } else {
1257             $filerecord->timemodified = $now;
1258         }
1260         $transaction = $DB->start_delegated_transaction();
1262         try {
1263             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1264         } catch (Exception $e) {
1265             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1266         }
1268         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1269             // there was specified the contenthash for a file already stored in moodle filepool
1270             if (empty($filerecord->filesize)) {
1271                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1272                 $filerecord->filesize = filesize($filepathname);
1273             } else {
1274                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1275             }
1276         } else {
1277             // atempt to get the result of last synchronisation for this reference
1278             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1279                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1280             if ($lastcontent) {
1281                 $filerecord->contenthash = $lastcontent->contenthash;
1282                 $filerecord->filesize = $lastcontent->filesize;
1283             } else {
1284                 // External file doesn't have content in moodle.
1285                 // So we create an empty file for it.
1286                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1287             }
1288         }
1290         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1292         try {
1293             $filerecord->id = $DB->insert_record('files', $filerecord);
1294         } catch (dml_exception $e) {
1295             if (!empty($newfile)) {
1296                 $this->deleted_file_cleanup($filerecord->contenthash);
1297             }
1298             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1299                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1300         }
1302         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1304         $transaction->allow_commit();
1306         // this will retrieve all reference information from DB as well
1307         return $this->get_file_by_id($filerecord->id);
1308     }
1310     /**
1311      * Creates new image file from existing.
1312      *
1313      * @param stdClass|array $filerecord object or array describing new file
1314      * @param int|stored_file $fid file id or stored file object
1315      * @param int $newwidth in pixels
1316      * @param int $newheight in pixels
1317      * @param bool $keepaspectratio whether or not keep aspect ratio
1318      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1319      * @return stored_file
1320      */
1321     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1322         if (!function_exists('imagecreatefromstring')) {
1323             //Most likely the GD php extension isn't installed
1324             //image conversion cannot succeed
1325             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1326         }
1328         if ($fid instanceof stored_file) {
1329             $fid = $fid->get_id();
1330         }
1332         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1334         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1335             throw new file_exception('storedfileproblem', 'File does not exist');
1336         }
1338         if (!$imageinfo = $file->get_imageinfo()) {
1339             throw new file_exception('storedfileproblem', 'File is not an image');
1340         }
1342         if (!isset($filerecord['filename'])) {
1343             $filerecord['filename'] = $file->get_filename();
1344         }
1346         if (!isset($filerecord['mimetype'])) {
1347             $filerecord['mimetype'] = $imageinfo['mimetype'];
1348         }
1350         $width    = $imageinfo['width'];
1351         $height   = $imageinfo['height'];
1352         $mimetype = $imageinfo['mimetype'];
1354         if ($keepaspectratio) {
1355             if (0 >= $newwidth and 0 >= $newheight) {
1356                 // no sizes specified
1357                 $newwidth  = $width;
1358                 $newheight = $height;
1360             } else if (0 < $newwidth and 0 < $newheight) {
1361                 $xheight = ($newwidth*($height/$width));
1362                 if ($xheight < $newheight) {
1363                     $newheight = (int)$xheight;
1364                 } else {
1365                     $newwidth = (int)($newheight*($width/$height));
1366                 }
1368             } else if (0 < $newwidth) {
1369                 $newheight = (int)($newwidth*($height/$width));
1371             } else { //0 < $newheight
1372                 $newwidth = (int)($newheight*($width/$height));
1373             }
1375         } else {
1376             if (0 >= $newwidth) {
1377                 $newwidth = $width;
1378             }
1379             if (0 >= $newheight) {
1380                 $newheight = $height;
1381             }
1382         }
1384         $img = imagecreatefromstring($file->get_content());
1385         if ($height != $newheight or $width != $newwidth) {
1386             $newimg = imagecreatetruecolor($newwidth, $newheight);
1387             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1388                 // weird
1389                 throw new file_exception('storedfileproblem', 'Can not resize image');
1390             }
1391             imagedestroy($img);
1392             $img = $newimg;
1393         }
1395         ob_start();
1396         switch ($filerecord['mimetype']) {
1397             case 'image/gif':
1398                 imagegif($img);
1399                 break;
1401             case 'image/jpeg':
1402                 if (is_null($quality)) {
1403                     imagejpeg($img);
1404                 } else {
1405                     imagejpeg($img, NULL, $quality);
1406                 }
1407                 break;
1409             case 'image/png':
1410                 $quality = (int)$quality;
1411                 imagepng($img, NULL, $quality, NULL);
1412                 break;
1414             default:
1415                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1416         }
1418         $content = ob_get_contents();
1419         ob_end_clean();
1420         imagedestroy($img);
1422         if (!$content) {
1423             throw new file_exception('storedfileproblem', 'Can not convert image');
1424         }
1426         return $this->create_file_from_string($filerecord, $content);
1427     }
1429     /**
1430      * Add file content to sha1 pool.
1431      *
1432      * @param string $pathname path to file
1433      * @param string $contenthash sha1 hash of content if known (performance only)
1434      * @return array (contenthash, filesize, newfile)
1435      */
1436     public function add_file_to_pool($pathname, $contenthash = NULL) {
1437         if (!is_readable($pathname)) {
1438             throw new file_exception('storedfilecannotread', '', $pathname);
1439         }
1441         if (is_null($contenthash)) {
1442             $contenthash = sha1_file($pathname);
1443         }
1445         $filesize = filesize($pathname);
1447         $hashpath = $this->path_from_hash($contenthash);
1448         $hashfile = "$hashpath/$contenthash";
1450         if (file_exists($hashfile)) {
1451             if (filesize($hashfile) !== $filesize) {
1452                 throw new file_pool_content_exception($contenthash);
1453             }
1454             $newfile = false;
1456         } else {
1457             if (!is_dir($hashpath)) {
1458                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1459                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1460                 }
1461             }
1462             $newfile = true;
1464             if (!copy($pathname, $hashfile)) {
1465                 throw new file_exception('storedfilecannotread', '', $pathname);
1466             }
1468             if (filesize($hashfile) !== $filesize) {
1469                 @unlink($hashfile);
1470                 throw new file_pool_content_exception($contenthash);
1471             }
1472             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1473         }
1476         return array($contenthash, $filesize, $newfile);
1477     }
1479     /**
1480      * Add string content to sha1 pool.
1481      *
1482      * @param string $content file content - binary string
1483      * @return array (contenthash, filesize, newfile)
1484      */
1485     public function add_string_to_pool($content) {
1486         $contenthash = sha1($content);
1487         $filesize = strlen($content); // binary length
1489         $hashpath = $this->path_from_hash($contenthash);
1490         $hashfile = "$hashpath/$contenthash";
1493         if (file_exists($hashfile)) {
1494             if (filesize($hashfile) !== $filesize) {
1495                 throw new file_pool_content_exception($contenthash);
1496             }
1497             $newfile = false;
1499         } else {
1500             if (!is_dir($hashpath)) {
1501                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1502                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1503                 }
1504             }
1505             $newfile = true;
1507             file_put_contents($hashfile, $content);
1509             if (filesize($hashfile) !== $filesize) {
1510                 @unlink($hashfile);
1511                 throw new file_pool_content_exception($contenthash);
1512             }
1513             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1514         }
1516         return array($contenthash, $filesize, $newfile);
1517     }
1519     /**
1520      * Serve file content using X-Sendfile header.
1521      * Please make sure that all headers are already sent
1522      * and the all access control checks passed.
1523      *
1524      * @param string $contenthash sah1 hash of the file content to be served
1525      * @return bool success
1526      */
1527     public function xsendfile($contenthash) {
1528         global $CFG;
1529         require_once("$CFG->libdir/xsendfilelib.php");
1531         $hashpath = $this->path_from_hash($contenthash);
1532         return xsendfile("$hashpath/$contenthash");
1533     }
1535     /**
1536      * Content exists
1537      *
1538      * @param string $contenthash
1539      * @return bool
1540      */
1541     public function content_exists($contenthash) {
1542         $dir = $this->path_from_hash($contenthash);
1543         $filepath = $dir . '/' . $contenthash;
1544         return file_exists($filepath);
1545     }
1547     /**
1548      * Return path to file with given hash.
1549      *
1550      * NOTE: must not be public, files in pool must not be modified
1551      *
1552      * @param string $contenthash content hash
1553      * @return string expected file location
1554      */
1555     protected function path_from_hash($contenthash) {
1556         $l1 = $contenthash[0].$contenthash[1];
1557         $l2 = $contenthash[2].$contenthash[3];
1558         return "$this->filedir/$l1/$l2";
1559     }
1561     /**
1562      * Return path to file with given hash.
1563      *
1564      * NOTE: must not be public, files in pool must not be modified
1565      *
1566      * @param string $contenthash content hash
1567      * @return string expected file location
1568      */
1569     protected function trash_path_from_hash($contenthash) {
1570         $l1 = $contenthash[0].$contenthash[1];
1571         $l2 = $contenthash[2].$contenthash[3];
1572         return "$this->trashdir/$l1/$l2";
1573     }
1575     /**
1576      * Tries to recover missing content of file from trash.
1577      *
1578      * @param stored_file $file stored_file instance
1579      * @return bool success
1580      */
1581     public function try_content_recovery($file) {
1582         $contenthash = $file->get_contenthash();
1583         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1584         if (!is_readable($trashfile)) {
1585             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1586                 return false;
1587             }
1588             // nice, at least alternative trash file in trash root exists
1589             $trashfile = $this->trashdir.'/'.$contenthash;
1590         }
1591         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1592             //weird, better fail early
1593             return false;
1594         }
1595         $contentdir  = $this->path_from_hash($contenthash);
1596         $contentfile = $contentdir.'/'.$contenthash;
1597         if (file_exists($contentfile)) {
1598             //strange, no need to recover anything
1599             return true;
1600         }
1601         if (!is_dir($contentdir)) {
1602             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1603                 return false;
1604             }
1605         }
1606         return rename($trashfile, $contentfile);
1607     }
1609     /**
1610      * Marks pool file as candidate for deleting.
1611      *
1612      * DO NOT call directly - reserved for core!!
1613      *
1614      * @param string $contenthash
1615      */
1616     public function deleted_file_cleanup($contenthash) {
1617         global $DB;
1619         //Note: this section is critical - in theory file could be reused at the same
1620         //      time, if this happens we can still recover the file from trash
1621         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1622             // file content is still used
1623             return;
1624         }
1625         //move content file to trash
1626         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1627         if (!file_exists($contentfile)) {
1628             //weird, but no problem
1629             return;
1630         }
1631         $trashpath = $this->trash_path_from_hash($contenthash);
1632         $trashfile = $trashpath.'/'.$contenthash;
1633         if (file_exists($trashfile)) {
1634             // we already have this content in trash, no need to move it there
1635             unlink($contentfile);
1636             return;
1637         }
1638         if (!is_dir($trashpath)) {
1639             mkdir($trashpath, $this->dirpermissions, true);
1640         }
1641         rename($contentfile, $trashfile);
1642         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1643     }
1645     /**
1646      * When user referring to a moodle file, we build the reference field
1647      *
1648      * @param array $params
1649      * @return string
1650      */
1651     public static function pack_reference($params) {
1652         $params = (array)$params;
1653         $reference = array();
1654         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1655         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1656         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1657         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1658         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);;
1659         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1660         return base64_encode(serialize($reference));
1661     }
1663     /**
1664      * Unpack reference field
1665      *
1666      * @param string $str
1667      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1668      * @throws file_reference_exception if the $str does not have the expected format
1669      * @return array
1670      */
1671     public static function unpack_reference($str, $cleanparams = false) {
1672         $decoded = base64_decode($str, true);
1673         if ($decoded === false) {
1674             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1675         }
1676         $params = @unserialize($decoded); // hide E_NOTICE
1677         if ($params === false) {
1678             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1679         }
1680         if (is_array($params) && $cleanparams) {
1681             $params = array(
1682                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1683                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1684                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1685                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1686                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1687                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1688             );
1689         }
1690         return $params;
1691     }
1693     /**
1694      * Returns all aliases that refer to some stored_file via the given reference
1695      *
1696      * All repositories that provide access to a stored_file are expected to use
1697      * {@link self::pack_reference()}. This method can't be used if the given reference
1698      * does not use this format or if you are looking for references to an external file
1699      * (for example it can't be used to search for all aliases that refer to a given
1700      * Dropbox or Box.net file).
1701      *
1702      * Aliases in user draft areas are excluded from the returned list.
1703      *
1704      * @param string $reference identification of the referenced file
1705      * @return array of stored_file indexed by its pathnamehash
1706      */
1707     public function search_references($reference) {
1708         global $DB;
1710         if (is_null($reference)) {
1711             throw new coding_exception('NULL is not a valid reference to an external file');
1712         }
1714         // Give {@link self::unpack_reference()} a chance to throw exception if the
1715         // reference is not in a valid format.
1716         self::unpack_reference($reference);
1718         $referencehash = sha1($reference);
1720         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1721                   FROM {files} f
1722                   JOIN {files_reference} r ON f.referencefileid = r.id
1723                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1724                  WHERE r.referencehash = ?
1725                        AND (f.component <> ? OR f.filearea <> ?)";
1727         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1728         $files = array();
1729         foreach ($rs as $filerecord) {
1730             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1731         }
1733         return $files;
1734     }
1736     /**
1737      * Returns the number of aliases that refer to some stored_file via the given reference
1738      *
1739      * All repositories that provide access to a stored_file are expected to use
1740      * {@link self::pack_reference()}. This method can't be used if the given reference
1741      * does not use this format or if you are looking for references to an external file
1742      * (for example it can't be used to count aliases that refer to a given Dropbox or
1743      * Box.net file).
1744      *
1745      * Aliases in user draft areas are not counted.
1746      *
1747      * @param string $reference identification of the referenced file
1748      * @return int
1749      */
1750     public function search_references_count($reference) {
1751         global $DB;
1753         if (is_null($reference)) {
1754             throw new coding_exception('NULL is not a valid reference to an external file');
1755         }
1757         // Give {@link self::unpack_reference()} a chance to throw exception if the
1758         // reference is not in a valid format.
1759         self::unpack_reference($reference);
1761         $referencehash = sha1($reference);
1763         $sql = "SELECT COUNT(f.id)
1764                   FROM {files} f
1765                   JOIN {files_reference} r ON f.referencefileid = r.id
1766                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1767                  WHERE r.referencehash = ?
1768                        AND (f.component <> ? OR f.filearea <> ?)";
1770         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
1771     }
1773     /**
1774      * Returns all aliases that link to the given stored_file
1775      *
1776      * Aliases in user draft areas are excluded from the returned list.
1777      *
1778      * @param stored_file $storedfile
1779      * @return array of stored_file
1780      */
1781     public function get_references_by_storedfile(stored_file $storedfile) {
1782         global $DB;
1784         $params = array();
1785         $params['contextid'] = $storedfile->get_contextid();
1786         $params['component'] = $storedfile->get_component();
1787         $params['filearea']  = $storedfile->get_filearea();
1788         $params['itemid']    = $storedfile->get_itemid();
1789         $params['filename']  = $storedfile->get_filename();
1790         $params['filepath']  = $storedfile->get_filepath();
1792         return $this->search_references(self::pack_reference($params));
1793     }
1795     /**
1796      * Returns the number of aliases that link to the given stored_file
1797      *
1798      * Aliases in user draft areas are not counted.
1799      *
1800      * @param stored_file $storedfile
1801      * @return int
1802      */
1803     public function get_references_count_by_storedfile(stored_file $storedfile) {
1804         global $DB;
1806         $params = array();
1807         $params['contextid'] = $storedfile->get_contextid();
1808         $params['component'] = $storedfile->get_component();
1809         $params['filearea']  = $storedfile->get_filearea();
1810         $params['itemid']    = $storedfile->get_itemid();
1811         $params['filename']  = $storedfile->get_filename();
1812         $params['filepath']  = $storedfile->get_filepath();
1814         return $this->search_references_count(self::pack_reference($params));
1815     }
1817     /**
1818      * Updates all files that are referencing this file with the new contenthash
1819      * and filesize
1820      *
1821      * @param stored_file $storedfile
1822      */
1823     public function update_references_to_storedfile(stored_file $storedfile) {
1824         global $CFG, $DB;
1825         $params = array();
1826         $params['contextid'] = $storedfile->get_contextid();
1827         $params['component'] = $storedfile->get_component();
1828         $params['filearea']  = $storedfile->get_filearea();
1829         $params['itemid']    = $storedfile->get_itemid();
1830         $params['filename']  = $storedfile->get_filename();
1831         $params['filepath']  = $storedfile->get_filepath();
1832         $reference = self::pack_reference($params);
1833         $referencehash = sha1($reference);
1835         $sql = "SELECT repositoryid, id FROM {files_reference}
1836                  WHERE referencehash = ? and reference = ?";
1837         $rs = $DB->get_recordset_sql($sql, array($referencehash, $reference));
1839         $now = time();
1840         foreach ($rs as $record) {
1841             require_once($CFG->dirroot.'/repository/lib.php');
1842             $repo = repository::get_instance($record->repositoryid);
1843             $lifetime = $repo->get_reference_file_lifetime($reference);
1844             $this->update_references($record->id, $now, $lifetime,
1845                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
1846         }
1847         $rs->close();
1848     }
1850     /**
1851      * Convert file alias to local file
1852      *
1853      * @throws moodle_exception if file could not be downloaded
1854      *
1855      * @param stored_file $storedfile a stored_file instances
1856      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
1857      * @return stored_file stored_file
1858      */
1859     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
1860         global $CFG;
1861         $storedfile->import_external_file_contents($maxbytes);
1862         $storedfile->delete_reference();
1863         return $storedfile;
1864     }
1866     /**
1867      * Return mimetype by given file pathname
1868      *
1869      * If file has a known extension, we return the mimetype based on extension.
1870      * Otherwise (when possible) we try to get the mimetype from file contents.
1871      *
1872      * @param string $pathname full path to the file
1873      * @param string $filename correct file name with extension, if omitted will be taken from $path
1874      * @return string
1875      */
1876     public static function mimetype($pathname, $filename = null) {
1877         if (empty($filename)) {
1878             $filename = $pathname;
1879         }
1880         $type = mimeinfo('type', $filename);
1881         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1882             $finfo = new finfo(FILEINFO_MIME_TYPE);
1883             $type = mimeinfo_from_type('type', $finfo->file($pathname));
1884         }
1885         return $type;
1886     }
1888     /**
1889      * Cron cleanup job.
1890      */
1891     public function cron() {
1892         global $CFG, $DB;
1894         // find out all stale draft areas (older than 4 days) and purge them
1895         // those are identified by time stamp of the /. root dir
1896         mtrace('Deleting old draft files... ', '');
1897         $old = time() - 60*60*24*4;
1898         $sql = "SELECT *
1899                   FROM {files}
1900                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1901                        AND timecreated < :old";
1902         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1903         foreach ($rs as $dir) {
1904             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1905         }
1906         $rs->close();
1907         mtrace('done.');
1909         // remove orphaned preview files (that is files in the core preview filearea without
1910         // the existing original file)
1911         mtrace('Deleting orphaned preview files... ', '');
1912         $sql = "SELECT p.*
1913                   FROM {files} p
1914              LEFT JOIN {files} o ON (p.filename = o.contenthash)
1915                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
1916                        AND o.id IS NULL";
1917         $syscontext = context_system::instance();
1918         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
1919         foreach ($rs as $orphan) {
1920             $file = $this->get_file_instance($orphan);
1921             if (!$file->is_directory()) {
1922                 $file->delete();
1923             }
1924         }
1925         $rs->close();
1926         mtrace('done.');
1928         // remove trash pool files once a day
1929         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1930         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1931             require_once($CFG->libdir.'/filelib.php');
1932             // Delete files that are associated with a context that no longer exists.
1933             mtrace('Cleaning up files from deleted contexts... ', '');
1934             $sql = "SELECT DISTINCT f.contextid
1935                     FROM {files} f
1936                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
1937                     WHERE c.id IS NULL";
1938             $rs = $DB->get_recordset_sql($sql);
1939             if ($rs->valid()) {
1940                 $fs = get_file_storage();
1941                 foreach ($rs as $ctx) {
1942                     $fs->delete_area_files($ctx->contextid);
1943                 }
1944             }
1945             $rs->close();
1946             mtrace('done.');
1948             mtrace('Deleting trash files... ', '');
1949             fulldelete($this->trashdir);
1950             set_config('fileslastcleanup', time());
1951             mtrace('done.');
1952         }
1953     }
1955     /**
1956      * Get the sql formated fields for a file instance to be created from a
1957      * {files} and {files_refernece} join.
1958      *
1959      * @param string $filesprefix the table prefix for the {files} table
1960      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
1961      * @return string the sql to go after a SELECT
1962      */
1963     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
1964         // Note, these fieldnames MUST NOT overlap between the two tables,
1965         // else problems like MDL-33172 occur.
1966         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
1967             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
1968             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
1970         $referencefields = array('repositoryid' => 'repositoryid',
1971             'reference' => 'reference',
1972             'lastsync' => 'referencelastsync',
1973             'lifetime' => 'referencelifetime');
1975         // id is specifically named to prevent overlaping between the two tables.
1976         $fields = array();
1977         $fields[] = $filesprefix.'.id AS id';
1978         foreach ($filefields as $field) {
1979             $fields[] = "{$filesprefix}.{$field}";
1980         }
1982         foreach ($referencefields as $field => $alias) {
1983             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
1984         }
1986         return implode(', ', $fields);
1987     }
1989     /**
1990      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
1991      *
1992      * If the record already exists, its id is returned. If there is no such record yet,
1993      * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
1994      *
1995      * @param int $repositoryid
1996      * @param string $reference
1997      * @return int
1998      */
1999     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2000         global $DB;
2002         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2004         if ($id !== false) {
2005             // bah, that was easy
2006             return $id;
2007         }
2009         // no such record yet, create one
2010         try {
2011             $id = $DB->insert_record('files_reference', array(
2012                 'repositoryid'  => $repositoryid,
2013                 'reference'     => $reference,
2014                 'referencehash' => sha1($reference),
2015                 'lastsync'      => $lastsync,
2016                 'lifetime'      => $lifetime));
2017         } catch (dml_exception $e) {
2018             // if inserting the new record failed, chances are that the race condition has just
2019             // occured and the unique index did not allow to create the second record with the same
2020             // repositoryid + reference combo
2021             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2022         }
2024         return $id;
2025     }
2027     /**
2028      * Returns the id of the record in {files_reference} that matches the passed parameters
2029      *
2030      * Depending on the required strictness, false can be returned. The behaviour is consistent
2031      * with standard DML methods.
2032      *
2033      * @param int $repositoryid
2034      * @param string $reference
2035      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2036      * @return int|bool
2037      */
2038     private function get_referencefileid($repositoryid, $reference, $strictness) {
2039         global $DB;
2041         return $DB->get_field('files_reference', 'id',
2042             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2043     }
2045     /**
2046      * Updates a reference to the external resource and all files that use it
2047      *
2048      * This function is called after synchronisation of an external file and updates the
2049      * contenthash, filesize and status of all files that reference this external file
2050      * as well as time last synchronised and sync lifetime (how long we don't need to call
2051      * synchronisation for this reference).
2052      *
2053      * @param int $referencefileid
2054      * @param int $lastsync
2055      * @param int $lifetime
2056      * @param string $contenthash
2057      * @param int $filesize
2058      * @param int $status 0 if ok or 666 if source is missing
2059      */
2060     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2061         global $DB;
2062         $referencefileid = clean_param($referencefileid, PARAM_INT);
2063         $lastsync = clean_param($lastsync, PARAM_INT);
2064         $lifetime = clean_param($lifetime, PARAM_INT);
2065         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2066         $filesize = clean_param($filesize, PARAM_INT);
2067         $status = clean_param($status, PARAM_INT);
2068         $params = array('contenthash' => $contenthash,
2069                     'filesize' => $filesize,
2070                     'status' => $status,
2071                     'referencefileid' => $referencefileid,
2072                     'lastsync' => $lastsync,
2073                     'lifetime' => $lifetime);
2074         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2075             status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
2076             WHERE referencefileid = :referencefileid', $params);
2077         $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2078         $DB->update_record('files_reference', (object)$data);
2079     }