Merge branch 'w08_MDL-38121_m25_plugincheck' of git://github.com/skodak/moodle
[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 = "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, '', 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         $result = $this->sort_area_tree($result);
484         return $result;
485     }
487     /**
488      * Sorts the result of {@link file_storage::get_area_tree()}.
489      *
490      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
491      * @return array of sorted results
492      */
493     protected function sort_area_tree($tree) {
494         foreach ($tree as $key => &$value) {
495             if ($key == 'subdirs') {
496                 $value = $this->sort_area_tree($value);
497                 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
498             } else if ($key == 'files') {
499                 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
500             }
501         }
502         return $tree;
503     }
505     /**
506      * Returns all files and optionally directories
507      *
508      * @param int $contextid context ID
509      * @param string $component component
510      * @param string $filearea file area
511      * @param int $itemid item ID
512      * @param int $filepath directory path
513      * @param bool $recursive include all subdirectories
514      * @param bool $includedirs include files and directories
515      * @param string $sort A fragment of SQL to use for sorting
516      * @return array of stored_files indexed by pathanmehash
517      */
518     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
519         global $DB;
521         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
522             return array();
523         }
525         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
527         if ($recursive) {
529             $dirs = $includedirs ? "" : "AND filename <> '.'";
530             $length = textlib::strlen($filepath);
532             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
533                       FROM {files} f
534                  LEFT JOIN {files_reference} r
535                            ON f.referencefileid = r.id
536                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
537                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
538                            AND f.id <> :dirid
539                            $dirs
540                            $orderby";
541             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
543             $files = array();
544             $dirs  = array();
545             $filerecords = $DB->get_records_sql($sql, $params);
546             foreach ($filerecords as $filerecord) {
547                 if ($filerecord->filename == '.') {
548                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
549                 } else {
550                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
551                 }
552             }
553             $result = array_merge($dirs, $files);
555         } else {
556             $result = array();
557             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
559             $length = textlib::strlen($filepath);
561             if ($includedirs) {
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
567                                AND f.itemid = :itemid AND f.filename = '.'
568                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
569                                AND f.id <> :dirid
570                                $orderby";
571                 $reqlevel = substr_count($filepath, '/') + 1;
572                 $filerecords = $DB->get_records_sql($sql, $params);
573                 foreach ($filerecords as $filerecord) {
574                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
575                         continue;
576                     }
577                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
578                 }
579             }
581             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
582                       FROM {files} f
583                  LEFT JOIN {files_reference} r
584                            ON f.referencefileid = r.id
585                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
586                            AND f.filepath = :filepath AND f.filename <> '.'
587                            $orderby";
589             $filerecords = $DB->get_records_sql($sql, $params);
590             foreach ($filerecords as $filerecord) {
591                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
592             }
593         }
595         return $result;
596     }
598     /**
599      * Delete all area files (optionally limited by itemid).
600      *
601      * @param int $contextid context ID
602      * @param string $component component
603      * @param string $filearea file area or all areas in context if not specified
604      * @param int $itemid item ID or all files if not specified
605      * @return bool success
606      */
607     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
608         global $DB;
610         $conditions = array('contextid'=>$contextid);
611         if ($component !== false) {
612             $conditions['component'] = $component;
613         }
614         if ($filearea !== false) {
615             $conditions['filearea'] = $filearea;
616         }
617         if ($itemid !== false) {
618             $conditions['itemid'] = $itemid;
619         }
621         $filerecords = $DB->get_records('files', $conditions);
622         foreach ($filerecords as $filerecord) {
623             $this->get_file_instance($filerecord)->delete();
624         }
626         return true; // BC only
627     }
629     /**
630      * Delete all the files from certain areas where itemid is limited by an
631      * arbitrary bit of SQL.
632      *
633      * @param int $contextid the id of the context the files belong to. Must be given.
634      * @param string $component the owning component. Must be given.
635      * @param string $filearea the file area name. Must be given.
636      * @param string $itemidstest an SQL fragment that the itemid must match. Used
637      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
638      *      and may not used named parameters called contextid, component or filearea.
639      * @param array $params any query params used by $itemidstest.
640      */
641     public function delete_area_files_select($contextid, $component,
642             $filearea, $itemidstest, array $params = null) {
643         global $DB;
645         $where = "contextid = :contextid
646                 AND component = :component
647                 AND filearea = :filearea
648                 AND itemid $itemidstest";
649         $params['contextid'] = $contextid;
650         $params['component'] = $component;
651         $params['filearea'] = $filearea;
653         $filerecords = $DB->get_recordset_select('files', $where, $params);
654         foreach ($filerecords as $filerecord) {
655             $this->get_file_instance($filerecord)->delete();
656         }
657         $filerecords->close();
658     }
660     /**
661      * Move all the files in a file area from one context to another.
662      *
663      * @param int $oldcontextid the context the files are being moved from.
664      * @param int $newcontextid the context the files are being moved to.
665      * @param string $component the plugin that these files belong to.
666      * @param string $filearea the name of the file area.
667      * @param int $itemid file item ID
668      * @return int the number of files moved, for information.
669      */
670     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
671         // Note, this code is based on some code that Petr wrote in
672         // forum_move_attachments in mod/forum/lib.php. I moved it here because
673         // I needed it in the question code too.
674         $count = 0;
676         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
677         foreach ($oldfiles as $oldfile) {
678             $filerecord = new stdClass();
679             $filerecord->contextid = $newcontextid;
680             $this->create_file_from_storedfile($filerecord, $oldfile);
681             $count += 1;
682         }
684         if ($count) {
685             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
686         }
688         return $count;
689     }
691     /**
692      * Recursively creates directory.
693      *
694      * @param int $contextid context ID
695      * @param string $component component
696      * @param string $filearea file area
697      * @param int $itemid item ID
698      * @param string $filepath file path
699      * @param int $userid the user ID
700      * @return bool success
701      */
702     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
703         global $DB;
705         // validate all parameters, we do not want any rubbish stored in database, right?
706         if (!is_number($contextid) or $contextid < 1) {
707             throw new file_exception('storedfileproblem', 'Invalid contextid');
708         }
710         $component = clean_param($component, PARAM_COMPONENT);
711         if (empty($component)) {
712             throw new file_exception('storedfileproblem', 'Invalid component');
713         }
715         $filearea = clean_param($filearea, PARAM_AREA);
716         if (empty($filearea)) {
717             throw new file_exception('storedfileproblem', 'Invalid filearea');
718         }
720         if (!is_number($itemid) or $itemid < 0) {
721             throw new file_exception('storedfileproblem', 'Invalid itemid');
722         }
724         $filepath = clean_param($filepath, PARAM_PATH);
725         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
726             // path must start and end with '/'
727             throw new file_exception('storedfileproblem', 'Invalid file path');
728         }
730         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
732         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
733             return $dir_info;
734         }
736         static $contenthash = null;
737         if (!$contenthash) {
738             $this->add_string_to_pool('');
739             $contenthash = sha1('');
740         }
742         $now = time();
744         $dir_record = new stdClass();
745         $dir_record->contextid = $contextid;
746         $dir_record->component = $component;
747         $dir_record->filearea  = $filearea;
748         $dir_record->itemid    = $itemid;
749         $dir_record->filepath  = $filepath;
750         $dir_record->filename  = '.';
751         $dir_record->contenthash  = $contenthash;
752         $dir_record->filesize  = 0;
754         $dir_record->timecreated  = $now;
755         $dir_record->timemodified = $now;
756         $dir_record->mimetype     = null;
757         $dir_record->userid       = $userid;
759         $dir_record->pathnamehash = $pathnamehash;
761         $DB->insert_record('files', $dir_record);
762         $dir_info = $this->get_file_by_hash($pathnamehash);
764         if ($filepath !== '/') {
765             //recurse to parent dirs
766             $filepath = trim($filepath, '/');
767             $filepath = explode('/', $filepath);
768             array_pop($filepath);
769             $filepath = implode('/', $filepath);
770             $filepath = ($filepath === '') ? '/' : "/$filepath/";
771             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
772         }
774         return $dir_info;
775     }
777     /**
778      * Add new local file based on existing local file.
779      *
780      * @param stdClass|array $filerecord object or array describing changes
781      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
782      * @return stored_file instance of newly created file
783      */
784     public function create_file_from_storedfile($filerecord, $fileorid) {
785         global $DB;
787         if ($fileorid instanceof stored_file) {
788             $fid = $fileorid->get_id();
789         } else {
790             $fid = $fileorid;
791         }
793         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
795         unset($filerecord['id']);
796         unset($filerecord['filesize']);
797         unset($filerecord['contenthash']);
798         unset($filerecord['pathnamehash']);
800         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
801                   FROM {files} f
802              LEFT JOIN {files_reference} r
803                        ON f.referencefileid = r.id
804                  WHERE f.id = ?";
806         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
807             throw new file_exception('storedfileproblem', 'File does not exist');
808         }
810         unset($newrecord->id);
812         foreach ($filerecord as $key => $value) {
813             // validate all parameters, we do not want any rubbish stored in database, right?
814             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
815                 throw new file_exception('storedfileproblem', 'Invalid contextid');
816             }
818             if ($key == 'component') {
819                 $value = clean_param($value, PARAM_COMPONENT);
820                 if (empty($value)) {
821                     throw new file_exception('storedfileproblem', 'Invalid component');
822                 }
823             }
825             if ($key == 'filearea') {
826                 $value = clean_param($value, PARAM_AREA);
827                 if (empty($value)) {
828                     throw new file_exception('storedfileproblem', 'Invalid filearea');
829                 }
830             }
832             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
833                 throw new file_exception('storedfileproblem', 'Invalid itemid');
834             }
837             if ($key == 'filepath') {
838                 $value = clean_param($value, PARAM_PATH);
839                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
840                     // path must start and end with '/'
841                     throw new file_exception('storedfileproblem', 'Invalid file path');
842                 }
843             }
845             if ($key == 'filename') {
846                 $value = clean_param($value, PARAM_FILE);
847                 if ($value === '') {
848                     // path must start and end with '/'
849                     throw new file_exception('storedfileproblem', 'Invalid file name');
850                 }
851             }
853             if ($key === 'timecreated' or $key === 'timemodified') {
854                 if (!is_number($value)) {
855                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
856                 }
857                 if ($value < 0) {
858                     //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)
859                     $value = 0;
860                 }
861             }
863             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
864                 $value = clean_param($value, PARAM_INT);
865             }
867             $newrecord->$key = $value;
868         }
870         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
872         if ($newrecord->filename === '.') {
873             // special case - only this function supports directories ;-)
874             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
875             // update the existing directory with the new data
876             $newrecord->id = $directory->get_id();
877             $DB->update_record('files', $newrecord);
878             return $this->get_file_instance($newrecord);
879         }
881         // note: referencefileid is copied from the original file so that
882         // creating a new file from an existing alias creates new alias implicitly.
883         // here we just check the database consistency.
884         if (!empty($newrecord->repositoryid)) {
885             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
886                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
887             }
888         }
890         try {
891             $newrecord->id = $DB->insert_record('files', $newrecord);
892         } catch (dml_exception $e) {
893             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
894                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
895         }
898         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
900         return $this->get_file_instance($newrecord);
901     }
903     /**
904      * Add new local file.
905      *
906      * @param stdClass|array $filerecord object or array describing file
907      * @param string $url the URL to the file
908      * @param array $options {@link download_file_content()} options
909      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
910      * @return stored_file
911      */
912     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
914         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
915         $filerecord = (object)$filerecord; // We support arrays too.
917         $headers        = isset($options['headers'])        ? $options['headers'] : null;
918         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
919         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
920         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
921         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
922         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
923         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
925         if (!isset($filerecord->filename)) {
926             $parts = explode('/', $url);
927             $filename = array_pop($parts);
928             $filerecord->filename = clean_param($filename, PARAM_FILE);
929         }
930         $source = !empty($filerecord->source) ? $filerecord->source : $url;
931         $filerecord->source = clean_param($source, PARAM_URL);
933         if ($usetempfile) {
934             check_dir_exists($this->tempdir);
935             $tmpfile = tempnam($this->tempdir, 'newfromurl');
936             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
937             if ($content === false) {
938                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
939             }
940             try {
941                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
942                 @unlink($tmpfile);
943                 return $newfile;
944             } catch (Exception $e) {
945                 @unlink($tmpfile);
946                 throw $e;
947             }
949         } else {
950             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
951             if ($content === false) {
952                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
953             }
954             return $this->create_file_from_string($filerecord, $content);
955         }
956     }
958     /**
959      * Add new local file.
960      *
961      * @param stdClass|array $filerecord object or array describing file
962      * @param string $pathname path to file or content of file
963      * @return stored_file
964      */
965     public function create_file_from_pathname($filerecord, $pathname) {
966         global $DB;
968         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
969         $filerecord = (object)$filerecord; // We support arrays too.
971         // validate all parameters, we do not want any rubbish stored in database, right?
972         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
973             throw new file_exception('storedfileproblem', 'Invalid contextid');
974         }
976         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
977         if (empty($filerecord->component)) {
978             throw new file_exception('storedfileproblem', 'Invalid component');
979         }
981         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
982         if (empty($filerecord->filearea)) {
983             throw new file_exception('storedfileproblem', 'Invalid filearea');
984         }
986         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
987             throw new file_exception('storedfileproblem', 'Invalid itemid');
988         }
990         if (!empty($filerecord->sortorder)) {
991             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
992                 $filerecord->sortorder = 0;
993             }
994         } else {
995             $filerecord->sortorder = 0;
996         }
998         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
999         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1000             // path must start and end with '/'
1001             throw new file_exception('storedfileproblem', 'Invalid file path');
1002         }
1004         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1005         if ($filerecord->filename === '') {
1006             // filename must not be empty
1007             throw new file_exception('storedfileproblem', 'Invalid file name');
1008         }
1010         $now = time();
1011         if (isset($filerecord->timecreated)) {
1012             if (!is_number($filerecord->timecreated)) {
1013                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1014             }
1015             if ($filerecord->timecreated < 0) {
1016                 //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)
1017                 $filerecord->timecreated = 0;
1018             }
1019         } else {
1020             $filerecord->timecreated = $now;
1021         }
1023         if (isset($filerecord->timemodified)) {
1024             if (!is_number($filerecord->timemodified)) {
1025                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1026             }
1027             if ($filerecord->timemodified < 0) {
1028                 //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)
1029                 $filerecord->timemodified = 0;
1030             }
1031         } else {
1032             $filerecord->timemodified = $now;
1033         }
1035         $newrecord = new stdClass();
1037         $newrecord->contextid = $filerecord->contextid;
1038         $newrecord->component = $filerecord->component;
1039         $newrecord->filearea  = $filerecord->filearea;
1040         $newrecord->itemid    = $filerecord->itemid;
1041         $newrecord->filepath  = $filerecord->filepath;
1042         $newrecord->filename  = $filerecord->filename;
1044         $newrecord->timecreated  = $filerecord->timecreated;
1045         $newrecord->timemodified = $filerecord->timemodified;
1046         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1047         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1048         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1049         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1050         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1051         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1052         $newrecord->sortorder    = $filerecord->sortorder;
1054         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1056         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1058         try {
1059             $newrecord->id = $DB->insert_record('files', $newrecord);
1060         } catch (dml_exception $e) {
1061             if ($newfile) {
1062                 $this->deleted_file_cleanup($newrecord->contenthash);
1063             }
1064             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1065                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1066         }
1068         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1070         return $this->get_file_instance($newrecord);
1071     }
1073     /**
1074      * Add new local file.
1075      *
1076      * @param stdClass|array $filerecord object or array describing file
1077      * @param string $content content of file
1078      * @return stored_file
1079      */
1080     public function create_file_from_string($filerecord, $content) {
1081         global $DB;
1083         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1084         $filerecord = (object)$filerecord; // We support arrays too.
1086         // validate all parameters, we do not want any rubbish stored in database, right?
1087         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1088             throw new file_exception('storedfileproblem', 'Invalid contextid');
1089         }
1091         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1092         if (empty($filerecord->component)) {
1093             throw new file_exception('storedfileproblem', 'Invalid component');
1094         }
1096         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1097         if (empty($filerecord->filearea)) {
1098             throw new file_exception('storedfileproblem', 'Invalid filearea');
1099         }
1101         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1102             throw new file_exception('storedfileproblem', 'Invalid itemid');
1103         }
1105         if (!empty($filerecord->sortorder)) {
1106             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1107                 $filerecord->sortorder = 0;
1108             }
1109         } else {
1110             $filerecord->sortorder = 0;
1111         }
1113         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1114         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1115             // path must start and end with '/'
1116             throw new file_exception('storedfileproblem', 'Invalid file path');
1117         }
1119         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1120         if ($filerecord->filename === '') {
1121             // path must start and end with '/'
1122             throw new file_exception('storedfileproblem', 'Invalid file name');
1123         }
1125         $now = time();
1126         if (isset($filerecord->timecreated)) {
1127             if (!is_number($filerecord->timecreated)) {
1128                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1129             }
1130             if ($filerecord->timecreated < 0) {
1131                 //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)
1132                 $filerecord->timecreated = 0;
1133             }
1134         } else {
1135             $filerecord->timecreated = $now;
1136         }
1138         if (isset($filerecord->timemodified)) {
1139             if (!is_number($filerecord->timemodified)) {
1140                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1141             }
1142             if ($filerecord->timemodified < 0) {
1143                 //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)
1144                 $filerecord->timemodified = 0;
1145             }
1146         } else {
1147             $filerecord->timemodified = $now;
1148         }
1150         $newrecord = new stdClass();
1152         $newrecord->contextid = $filerecord->contextid;
1153         $newrecord->component = $filerecord->component;
1154         $newrecord->filearea  = $filerecord->filearea;
1155         $newrecord->itemid    = $filerecord->itemid;
1156         $newrecord->filepath  = $filerecord->filepath;
1157         $newrecord->filename  = $filerecord->filename;
1159         $newrecord->timecreated  = $filerecord->timecreated;
1160         $newrecord->timemodified = $filerecord->timemodified;
1161         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1162         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1163         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1164         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1165         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1166         $newrecord->sortorder    = $filerecord->sortorder;
1168         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1169         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1170         // get mimetype by magic bytes
1171         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1173         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1175         try {
1176             $newrecord->id = $DB->insert_record('files', $newrecord);
1177         } catch (dml_exception $e) {
1178             if ($newfile) {
1179                 $this->deleted_file_cleanup($newrecord->contenthash);
1180             }
1181             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1182                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1183         }
1185         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1187         return $this->get_file_instance($newrecord);
1188     }
1190     /**
1191      * Create a new alias/shortcut file from file reference information
1192      *
1193      * @param stdClass|array $filerecord object or array describing the new file
1194      * @param int $repositoryid the id of the repository that provides the original file
1195      * @param string $reference the information required by the repository to locate the original file
1196      * @param array $options options for creating the new file
1197      * @return stored_file
1198      */
1199     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1200         global $DB;
1202         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1203         $filerecord = (object)$filerecord; // We support arrays too.
1205         // validate all parameters, we do not want any rubbish stored in database, right?
1206         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1207             throw new file_exception('storedfileproblem', 'Invalid contextid');
1208         }
1210         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1211         if (empty($filerecord->component)) {
1212             throw new file_exception('storedfileproblem', 'Invalid component');
1213         }
1215         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1216         if (empty($filerecord->filearea)) {
1217             throw new file_exception('storedfileproblem', 'Invalid filearea');
1218         }
1220         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1221             throw new file_exception('storedfileproblem', 'Invalid itemid');
1222         }
1224         if (!empty($filerecord->sortorder)) {
1225             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1226                 $filerecord->sortorder = 0;
1227             }
1228         } else {
1229             $filerecord->sortorder = 0;
1230         }
1232         // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
1233         unset($filerecord->referencelastsync);
1234         unset($filerecord->referencelifetime);
1236         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1237         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1238         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1239         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1240         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1241         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1242         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1243         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1244             // Path must start and end with '/'.
1245             throw new file_exception('storedfileproblem', 'Invalid file path');
1246         }
1248         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1249         if ($filerecord->filename === '') {
1250             // Path must start and end with '/'.
1251             throw new file_exception('storedfileproblem', 'Invalid file name');
1252         }
1254         $now = time();
1255         if (isset($filerecord->timecreated)) {
1256             if (!is_number($filerecord->timecreated)) {
1257                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1258             }
1259             if ($filerecord->timecreated < 0) {
1260                 // 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)
1261                 $filerecord->timecreated = 0;
1262             }
1263         } else {
1264             $filerecord->timecreated = $now;
1265         }
1267         if (isset($filerecord->timemodified)) {
1268             if (!is_number($filerecord->timemodified)) {
1269                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1270             }
1271             if ($filerecord->timemodified < 0) {
1272                 // 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)
1273                 $filerecord->timemodified = 0;
1274             }
1275         } else {
1276             $filerecord->timemodified = $now;
1277         }
1279         $transaction = $DB->start_delegated_transaction();
1281         try {
1282             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1283         } catch (Exception $e) {
1284             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1285         }
1287         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1288             // there was specified the contenthash for a file already stored in moodle filepool
1289             if (empty($filerecord->filesize)) {
1290                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1291                 $filerecord->filesize = filesize($filepathname);
1292             } else {
1293                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1294             }
1295         } else {
1296             // atempt to get the result of last synchronisation for this reference
1297             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1298                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1299             if ($lastcontent) {
1300                 $filerecord->contenthash = $lastcontent->contenthash;
1301                 $filerecord->filesize = $lastcontent->filesize;
1302             } else {
1303                 // External file doesn't have content in moodle.
1304                 // So we create an empty file for it.
1305                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1306             }
1307         }
1309         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1311         try {
1312             $filerecord->id = $DB->insert_record('files', $filerecord);
1313         } catch (dml_exception $e) {
1314             if (!empty($newfile)) {
1315                 $this->deleted_file_cleanup($filerecord->contenthash);
1316             }
1317             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1318                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1319         }
1321         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1323         $transaction->allow_commit();
1325         // this will retrieve all reference information from DB as well
1326         return $this->get_file_by_id($filerecord->id);
1327     }
1329     /**
1330      * Creates new image file from existing.
1331      *
1332      * @param stdClass|array $filerecord object or array describing new file
1333      * @param int|stored_file $fid file id or stored file object
1334      * @param int $newwidth in pixels
1335      * @param int $newheight in pixels
1336      * @param bool $keepaspectratio whether or not keep aspect ratio
1337      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1338      * @return stored_file
1339      */
1340     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1341         if (!function_exists('imagecreatefromstring')) {
1342             //Most likely the GD php extension isn't installed
1343             //image conversion cannot succeed
1344             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1345         }
1347         if ($fid instanceof stored_file) {
1348             $fid = $fid->get_id();
1349         }
1351         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1353         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1354             throw new file_exception('storedfileproblem', 'File does not exist');
1355         }
1357         if (!$imageinfo = $file->get_imageinfo()) {
1358             throw new file_exception('storedfileproblem', 'File is not an image');
1359         }
1361         if (!isset($filerecord['filename'])) {
1362             $filerecord['filename'] = $file->get_filename();
1363         }
1365         if (!isset($filerecord['mimetype'])) {
1366             $filerecord['mimetype'] = $imageinfo['mimetype'];
1367         }
1369         $width    = $imageinfo['width'];
1370         $height   = $imageinfo['height'];
1371         $mimetype = $imageinfo['mimetype'];
1373         if ($keepaspectratio) {
1374             if (0 >= $newwidth and 0 >= $newheight) {
1375                 // no sizes specified
1376                 $newwidth  = $width;
1377                 $newheight = $height;
1379             } else if (0 < $newwidth and 0 < $newheight) {
1380                 $xheight = ($newwidth*($height/$width));
1381                 if ($xheight < $newheight) {
1382                     $newheight = (int)$xheight;
1383                 } else {
1384                     $newwidth = (int)($newheight*($width/$height));
1385                 }
1387             } else if (0 < $newwidth) {
1388                 $newheight = (int)($newwidth*($height/$width));
1390             } else { //0 < $newheight
1391                 $newwidth = (int)($newheight*($width/$height));
1392             }
1394         } else {
1395             if (0 >= $newwidth) {
1396                 $newwidth = $width;
1397             }
1398             if (0 >= $newheight) {
1399                 $newheight = $height;
1400             }
1401         }
1403         $img = imagecreatefromstring($file->get_content());
1404         if ($height != $newheight or $width != $newwidth) {
1405             $newimg = imagecreatetruecolor($newwidth, $newheight);
1406             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1407                 // weird
1408                 throw new file_exception('storedfileproblem', 'Can not resize image');
1409             }
1410             imagedestroy($img);
1411             $img = $newimg;
1412         }
1414         ob_start();
1415         switch ($filerecord['mimetype']) {
1416             case 'image/gif':
1417                 imagegif($img);
1418                 break;
1420             case 'image/jpeg':
1421                 if (is_null($quality)) {
1422                     imagejpeg($img);
1423                 } else {
1424                     imagejpeg($img, NULL, $quality);
1425                 }
1426                 break;
1428             case 'image/png':
1429                 $quality = (int)$quality;
1430                 imagepng($img, NULL, $quality, NULL);
1431                 break;
1433             default:
1434                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1435         }
1437         $content = ob_get_contents();
1438         ob_end_clean();
1439         imagedestroy($img);
1441         if (!$content) {
1442             throw new file_exception('storedfileproblem', 'Can not convert image');
1443         }
1445         return $this->create_file_from_string($filerecord, $content);
1446     }
1448     /**
1449      * Add file content to sha1 pool.
1450      *
1451      * @param string $pathname path to file
1452      * @param string $contenthash sha1 hash of content if known (performance only)
1453      * @return array (contenthash, filesize, newfile)
1454      */
1455     public function add_file_to_pool($pathname, $contenthash = NULL) {
1456         if (!is_readable($pathname)) {
1457             throw new file_exception('storedfilecannotread', '', $pathname);
1458         }
1460         if (is_null($contenthash)) {
1461             $contenthash = sha1_file($pathname);
1462         }
1464         $filesize = filesize($pathname);
1466         $hashpath = $this->path_from_hash($contenthash);
1467         $hashfile = "$hashpath/$contenthash";
1469         if (file_exists($hashfile)) {
1470             if (filesize($hashfile) !== $filesize) {
1471                 throw new file_pool_content_exception($contenthash);
1472             }
1473             $newfile = false;
1475         } else {
1476             if (!is_dir($hashpath)) {
1477                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1478                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1479                 }
1480             }
1481             $newfile = true;
1483             if (!copy($pathname, $hashfile)) {
1484                 throw new file_exception('storedfilecannotread', '', $pathname);
1485             }
1487             if (filesize($hashfile) !== $filesize) {
1488                 @unlink($hashfile);
1489                 throw new file_pool_content_exception($contenthash);
1490             }
1491             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1492         }
1495         return array($contenthash, $filesize, $newfile);
1496     }
1498     /**
1499      * Add string content to sha1 pool.
1500      *
1501      * @param string $content file content - binary string
1502      * @return array (contenthash, filesize, newfile)
1503      */
1504     public function add_string_to_pool($content) {
1505         $contenthash = sha1($content);
1506         $filesize = strlen($content); // binary length
1508         $hashpath = $this->path_from_hash($contenthash);
1509         $hashfile = "$hashpath/$contenthash";
1512         if (file_exists($hashfile)) {
1513             if (filesize($hashfile) !== $filesize) {
1514                 throw new file_pool_content_exception($contenthash);
1515             }
1516             $newfile = false;
1518         } else {
1519             if (!is_dir($hashpath)) {
1520                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1521                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1522                 }
1523             }
1524             $newfile = true;
1526             file_put_contents($hashfile, $content);
1528             if (filesize($hashfile) !== $filesize) {
1529                 @unlink($hashfile);
1530                 throw new file_pool_content_exception($contenthash);
1531             }
1532             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1533         }
1535         return array($contenthash, $filesize, $newfile);
1536     }
1538     /**
1539      * Serve file content using X-Sendfile header.
1540      * Please make sure that all headers are already sent
1541      * and the all access control checks passed.
1542      *
1543      * @param string $contenthash sah1 hash of the file content to be served
1544      * @return bool success
1545      */
1546     public function xsendfile($contenthash) {
1547         global $CFG;
1548         require_once("$CFG->libdir/xsendfilelib.php");
1550         $hashpath = $this->path_from_hash($contenthash);
1551         return xsendfile("$hashpath/$contenthash");
1552     }
1554     /**
1555      * Content exists
1556      *
1557      * @param string $contenthash
1558      * @return bool
1559      */
1560     public function content_exists($contenthash) {
1561         $dir = $this->path_from_hash($contenthash);
1562         $filepath = $dir . '/' . $contenthash;
1563         return file_exists($filepath);
1564     }
1566     /**
1567      * Return path to file with given hash.
1568      *
1569      * NOTE: must not be public, files in pool must not be modified
1570      *
1571      * @param string $contenthash content hash
1572      * @return string expected file location
1573      */
1574     protected function path_from_hash($contenthash) {
1575         $l1 = $contenthash[0].$contenthash[1];
1576         $l2 = $contenthash[2].$contenthash[3];
1577         return "$this->filedir/$l1/$l2";
1578     }
1580     /**
1581      * Return path to file with given hash.
1582      *
1583      * NOTE: must not be public, files in pool must not be modified
1584      *
1585      * @param string $contenthash content hash
1586      * @return string expected file location
1587      */
1588     protected function trash_path_from_hash($contenthash) {
1589         $l1 = $contenthash[0].$contenthash[1];
1590         $l2 = $contenthash[2].$contenthash[3];
1591         return "$this->trashdir/$l1/$l2";
1592     }
1594     /**
1595      * Tries to recover missing content of file from trash.
1596      *
1597      * @param stored_file $file stored_file instance
1598      * @return bool success
1599      */
1600     public function try_content_recovery($file) {
1601         $contenthash = $file->get_contenthash();
1602         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1603         if (!is_readable($trashfile)) {
1604             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1605                 return false;
1606             }
1607             // nice, at least alternative trash file in trash root exists
1608             $trashfile = $this->trashdir.'/'.$contenthash;
1609         }
1610         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1611             //weird, better fail early
1612             return false;
1613         }
1614         $contentdir  = $this->path_from_hash($contenthash);
1615         $contentfile = $contentdir.'/'.$contenthash;
1616         if (file_exists($contentfile)) {
1617             //strange, no need to recover anything
1618             return true;
1619         }
1620         if (!is_dir($contentdir)) {
1621             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1622                 return false;
1623             }
1624         }
1625         return rename($trashfile, $contentfile);
1626     }
1628     /**
1629      * Marks pool file as candidate for deleting.
1630      *
1631      * DO NOT call directly - reserved for core!!
1632      *
1633      * @param string $contenthash
1634      */
1635     public function deleted_file_cleanup($contenthash) {
1636         global $DB;
1638         //Note: this section is critical - in theory file could be reused at the same
1639         //      time, if this happens we can still recover the file from trash
1640         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1641             // file content is still used
1642             return;
1643         }
1644         //move content file to trash
1645         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1646         if (!file_exists($contentfile)) {
1647             //weird, but no problem
1648             return;
1649         }
1650         $trashpath = $this->trash_path_from_hash($contenthash);
1651         $trashfile = $trashpath.'/'.$contenthash;
1652         if (file_exists($trashfile)) {
1653             // we already have this content in trash, no need to move it there
1654             unlink($contentfile);
1655             return;
1656         }
1657         if (!is_dir($trashpath)) {
1658             mkdir($trashpath, $this->dirpermissions, true);
1659         }
1660         rename($contentfile, $trashfile);
1661         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1662     }
1664     /**
1665      * When user referring to a moodle file, we build the reference field
1666      *
1667      * @param array $params
1668      * @return string
1669      */
1670     public static function pack_reference($params) {
1671         $params = (array)$params;
1672         $reference = array();
1673         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1674         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1675         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1676         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1677         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1678         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1679         return base64_encode(serialize($reference));
1680     }
1682     /**
1683      * Unpack reference field
1684      *
1685      * @param string $str
1686      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1687      * @throws file_reference_exception if the $str does not have the expected format
1688      * @return array
1689      */
1690     public static function unpack_reference($str, $cleanparams = false) {
1691         $decoded = base64_decode($str, true);
1692         if ($decoded === false) {
1693             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1694         }
1695         $params = @unserialize($decoded); // hide E_NOTICE
1696         if ($params === false) {
1697             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1698         }
1699         if (is_array($params) && $cleanparams) {
1700             $params = array(
1701                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1702                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1703                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1704                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1705                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1706                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1707             );
1708         }
1709         return $params;
1710     }
1712     /**
1713      * Returns all aliases that refer to some stored_file via the given reference
1714      *
1715      * All repositories that provide access to a stored_file are expected to use
1716      * {@link self::pack_reference()}. This method can't be used if the given reference
1717      * does not use this format or if you are looking for references to an external file
1718      * (for example it can't be used to search for all aliases that refer to a given
1719      * Dropbox or Box.net file).
1720      *
1721      * Aliases in user draft areas are excluded from the returned list.
1722      *
1723      * @param string $reference identification of the referenced file
1724      * @return array of stored_file indexed by its pathnamehash
1725      */
1726     public function search_references($reference) {
1727         global $DB;
1729         if (is_null($reference)) {
1730             throw new coding_exception('NULL is not a valid reference to an external file');
1731         }
1733         // Give {@link self::unpack_reference()} a chance to throw exception if the
1734         // reference is not in a valid format.
1735         self::unpack_reference($reference);
1737         $referencehash = sha1($reference);
1739         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1740                   FROM {files} f
1741                   JOIN {files_reference} r ON f.referencefileid = r.id
1742                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1743                  WHERE r.referencehash = ?
1744                        AND (f.component <> ? OR f.filearea <> ?)";
1746         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1747         $files = array();
1748         foreach ($rs as $filerecord) {
1749             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1750         }
1752         return $files;
1753     }
1755     /**
1756      * Returns the number of aliases that refer to some stored_file via the given reference
1757      *
1758      * All repositories that provide access to a stored_file are expected to use
1759      * {@link self::pack_reference()}. This method can't be used if the given reference
1760      * does not use this format or if you are looking for references to an external file
1761      * (for example it can't be used to count aliases that refer to a given Dropbox or
1762      * Box.net file).
1763      *
1764      * Aliases in user draft areas are not counted.
1765      *
1766      * @param string $reference identification of the referenced file
1767      * @return int
1768      */
1769     public function search_references_count($reference) {
1770         global $DB;
1772         if (is_null($reference)) {
1773             throw new coding_exception('NULL is not a valid reference to an external file');
1774         }
1776         // Give {@link self::unpack_reference()} a chance to throw exception if the
1777         // reference is not in a valid format.
1778         self::unpack_reference($reference);
1780         $referencehash = sha1($reference);
1782         $sql = "SELECT COUNT(f.id)
1783                   FROM {files} f
1784                   JOIN {files_reference} r ON f.referencefileid = r.id
1785                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1786                  WHERE r.referencehash = ?
1787                        AND (f.component <> ? OR f.filearea <> ?)";
1789         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
1790     }
1792     /**
1793      * Returns all aliases that link to the given stored_file
1794      *
1795      * Aliases in user draft areas are excluded from the returned list.
1796      *
1797      * @param stored_file $storedfile
1798      * @return array of stored_file
1799      */
1800     public function get_references_by_storedfile(stored_file $storedfile) {
1801         global $DB;
1803         $params = array();
1804         $params['contextid'] = $storedfile->get_contextid();
1805         $params['component'] = $storedfile->get_component();
1806         $params['filearea']  = $storedfile->get_filearea();
1807         $params['itemid']    = $storedfile->get_itemid();
1808         $params['filename']  = $storedfile->get_filename();
1809         $params['filepath']  = $storedfile->get_filepath();
1811         return $this->search_references(self::pack_reference($params));
1812     }
1814     /**
1815      * Returns the number of aliases that link to the given stored_file
1816      *
1817      * Aliases in user draft areas are not counted.
1818      *
1819      * @param stored_file $storedfile
1820      * @return int
1821      */
1822     public function get_references_count_by_storedfile(stored_file $storedfile) {
1823         global $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();
1833         return $this->search_references_count(self::pack_reference($params));
1834     }
1836     /**
1837      * Updates all files that are referencing this file with the new contenthash
1838      * and filesize
1839      *
1840      * @param stored_file $storedfile
1841      */
1842     public function update_references_to_storedfile(stored_file $storedfile) {
1843         global $CFG, $DB;
1844         $params = array();
1845         $params['contextid'] = $storedfile->get_contextid();
1846         $params['component'] = $storedfile->get_component();
1847         $params['filearea']  = $storedfile->get_filearea();
1848         $params['itemid']    = $storedfile->get_itemid();
1849         $params['filename']  = $storedfile->get_filename();
1850         $params['filepath']  = $storedfile->get_filepath();
1851         $reference = self::pack_reference($params);
1852         $referencehash = sha1($reference);
1854         $sql = "SELECT repositoryid, id FROM {files_reference}
1855                  WHERE referencehash = ?";
1856         $rs = $DB->get_recordset_sql($sql, array($referencehash));
1858         $now = time();
1859         foreach ($rs as $record) {
1860             require_once($CFG->dirroot.'/repository/lib.php');
1861             $repo = repository::get_instance($record->repositoryid);
1862             $lifetime = $repo->get_reference_file_lifetime($reference);
1863             $this->update_references($record->id, $now, $lifetime,
1864                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
1865         }
1866         $rs->close();
1867     }
1869     /**
1870      * Convert file alias to local file
1871      *
1872      * @throws moodle_exception if file could not be downloaded
1873      *
1874      * @param stored_file $storedfile a stored_file instances
1875      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
1876      * @return stored_file stored_file
1877      */
1878     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
1879         global $CFG;
1880         $storedfile->import_external_file_contents($maxbytes);
1881         $storedfile->delete_reference();
1882         return $storedfile;
1883     }
1885     /**
1886      * Return mimetype by given file pathname
1887      *
1888      * If file has a known extension, we return the mimetype based on extension.
1889      * Otherwise (when possible) we try to get the mimetype from file contents.
1890      *
1891      * @param string $pathname full path to the file
1892      * @param string $filename correct file name with extension, if omitted will be taken from $path
1893      * @return string
1894      */
1895     public static function mimetype($pathname, $filename = null) {
1896         if (empty($filename)) {
1897             $filename = $pathname;
1898         }
1899         $type = mimeinfo('type', $filename);
1900         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1901             $finfo = new finfo(FILEINFO_MIME_TYPE);
1902             $type = mimeinfo_from_type('type', $finfo->file($pathname));
1903         }
1904         return $type;
1905     }
1907     /**
1908      * Cron cleanup job.
1909      */
1910     public function cron() {
1911         global $CFG, $DB;
1913         // find out all stale draft areas (older than 4 days) and purge them
1914         // those are identified by time stamp of the /. root dir
1915         mtrace('Deleting old draft files... ', '');
1916         $old = time() - 60*60*24*4;
1917         $sql = "SELECT *
1918                   FROM {files}
1919                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1920                        AND timecreated < :old";
1921         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1922         foreach ($rs as $dir) {
1923             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1924         }
1925         $rs->close();
1926         mtrace('done.');
1928         // remove orphaned preview files (that is files in the core preview filearea without
1929         // the existing original file)
1930         mtrace('Deleting orphaned preview files... ', '');
1931         $sql = "SELECT p.*
1932                   FROM {files} p
1933              LEFT JOIN {files} o ON (p.filename = o.contenthash)
1934                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
1935                        AND o.id IS NULL";
1936         $syscontext = context_system::instance();
1937         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
1938         foreach ($rs as $orphan) {
1939             $file = $this->get_file_instance($orphan);
1940             if (!$file->is_directory()) {
1941                 $file->delete();
1942             }
1943         }
1944         $rs->close();
1945         mtrace('done.');
1947         // remove trash pool files once a day
1948         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1949         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1950             require_once($CFG->libdir.'/filelib.php');
1951             // Delete files that are associated with a context that no longer exists.
1952             mtrace('Cleaning up files from deleted contexts... ', '');
1953             $sql = "SELECT DISTINCT f.contextid
1954                     FROM {files} f
1955                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
1956                     WHERE c.id IS NULL";
1957             $rs = $DB->get_recordset_sql($sql);
1958             if ($rs->valid()) {
1959                 $fs = get_file_storage();
1960                 foreach ($rs as $ctx) {
1961                     $fs->delete_area_files($ctx->contextid);
1962                 }
1963             }
1964             $rs->close();
1965             mtrace('done.');
1967             mtrace('Deleting trash files... ', '');
1968             fulldelete($this->trashdir);
1969             set_config('fileslastcleanup', time());
1970             mtrace('done.');
1971         }
1972     }
1974     /**
1975      * Get the sql formated fields for a file instance to be created from a
1976      * {files} and {files_refernece} join.
1977      *
1978      * @param string $filesprefix the table prefix for the {files} table
1979      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
1980      * @return string the sql to go after a SELECT
1981      */
1982     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
1983         // Note, these fieldnames MUST NOT overlap between the two tables,
1984         // else problems like MDL-33172 occur.
1985         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
1986             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
1987             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
1989         $referencefields = array('repositoryid' => 'repositoryid',
1990             'reference' => 'reference',
1991             'lastsync' => 'referencelastsync',
1992             'lifetime' => 'referencelifetime');
1994         // id is specifically named to prevent overlaping between the two tables.
1995         $fields = array();
1996         $fields[] = $filesprefix.'.id AS id';
1997         foreach ($filefields as $field) {
1998             $fields[] = "{$filesprefix}.{$field}";
1999         }
2001         foreach ($referencefields as $field => $alias) {
2002             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2003         }
2005         return implode(', ', $fields);
2006     }
2008     /**
2009      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2010      *
2011      * If the record already exists, its id is returned. If there is no such record yet,
2012      * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
2013      *
2014      * @param int $repositoryid
2015      * @param string $reference
2016      * @return int
2017      */
2018     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2019         global $DB;
2021         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2023         if ($id !== false) {
2024             // bah, that was easy
2025             return $id;
2026         }
2028         // no such record yet, create one
2029         try {
2030             $id = $DB->insert_record('files_reference', array(
2031                 'repositoryid'  => $repositoryid,
2032                 'reference'     => $reference,
2033                 'referencehash' => sha1($reference),
2034                 'lastsync'      => $lastsync,
2035                 'lifetime'      => $lifetime));
2036         } catch (dml_exception $e) {
2037             // if inserting the new record failed, chances are that the race condition has just
2038             // occured and the unique index did not allow to create the second record with the same
2039             // repositoryid + reference combo
2040             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2041         }
2043         return $id;
2044     }
2046     /**
2047      * Returns the id of the record in {files_reference} that matches the passed parameters
2048      *
2049      * Depending on the required strictness, false can be returned. The behaviour is consistent
2050      * with standard DML methods.
2051      *
2052      * @param int $repositoryid
2053      * @param string $reference
2054      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2055      * @return int|bool
2056      */
2057     private function get_referencefileid($repositoryid, $reference, $strictness) {
2058         global $DB;
2060         return $DB->get_field('files_reference', 'id',
2061             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2062     }
2064     /**
2065      * Updates a reference to the external resource and all files that use it
2066      *
2067      * This function is called after synchronisation of an external file and updates the
2068      * contenthash, filesize and status of all files that reference this external file
2069      * as well as time last synchronised and sync lifetime (how long we don't need to call
2070      * synchronisation for this reference).
2071      *
2072      * @param int $referencefileid
2073      * @param int $lastsync
2074      * @param int $lifetime
2075      * @param string $contenthash
2076      * @param int $filesize
2077      * @param int $status 0 if ok or 666 if source is missing
2078      */
2079     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2080         global $DB;
2081         $referencefileid = clean_param($referencefileid, PARAM_INT);
2082         $lastsync = clean_param($lastsync, PARAM_INT);
2083         $lifetime = clean_param($lifetime, PARAM_INT);
2084         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2085         $filesize = clean_param($filesize, PARAM_INT);
2086         $status = clean_param($status, PARAM_INT);
2087         $params = array('contenthash' => $contenthash,
2088                     'filesize' => $filesize,
2089                     'status' => $status,
2090                     'referencefileid' => $referencefileid,
2091                     'lastsync' => $lastsync,
2092                     'lifetime' => $lifetime);
2093         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2094             status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
2095             WHERE referencefileid = :referencefileid', $params);
2096         $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2097         $DB->update_record('files_reference', (object)$data);
2098     }