MDL-37792 Course - Fix upgrade step and set version number for completion field fix.
[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      * Return an available file name.
185      *
186      * This will return the next available file name in the area, adding/incrementing a suffix
187      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
188      *
189      * If the file name passed is available without modification, it is returned as is.
190      *
191      * @param int $contextid context ID.
192      * @param string $component component.
193      * @param string $filearea file area.
194      * @param int $itemid area item ID.
195      * @param string $filepath the file path.
196      * @param string $filename the file name.
197      * @return string available file name.
198      * @throws coding_exception if the file name is invalid.
199      * @since 2.5
200      */
201     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
202         global $DB;
204         // Do not accept '.' or an empty file name (zero is acceptable).
205         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
206             throw new coding_exception('Invalid file name passed', $filename);
207         }
209         // The file does not exist, we return the same file name.
210         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
211             return $filename;
212         }
214         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
215         $pathinfo = pathinfo($filename);
216         $basename = $pathinfo['filename'];
217         $matches = array();
218         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
219             $basename = $matches[1];
220         }
222         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
223         if (isset($pathinfo['extension'])) {
224             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
225         }
227         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
228         $filenamelen = $DB->sql_length('f.filename');
229         $sql = "SELECT filename
230                 FROM {files} f
231                 WHERE
232                     f.contextid = :contextid AND
233                     f.component = :component AND
234                     f.filearea = :filearea AND
235                     f.itemid = :itemid AND
236                     f.filepath = :filepath AND
237                     $filenamelikesql
238                 ORDER BY
239                     $filenamelen DESC,
240                     f.filename DESC";
241         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
242                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
243         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
245         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
246         // would both be returned, but only the one only containing digits should be used.
247         $number = 1;
248         foreach ($results as $result) {
249             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
250             $matches = array();
251             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
252                 $number = $matches[2] + 1;
253                 break;
254             }
255         }
257         // Constructing the new filename.
258         $newfilename = $basename . ' (' . $number . ')';
259         if (isset($pathinfo['extension'])) {
260             $newfilename .= '.' . $pathinfo['extension'];
261         }
263         return $newfilename;
264     }
266     /**
267      * Generates a preview image for the stored file
268      *
269      * @param stored_file $file the file we want to preview
270      * @param string $mode preview mode, eg. 'thumb'
271      * @return stored_file|bool the newly created preview file or false
272      */
273     protected function create_file_preview(stored_file $file, $mode) {
275         $mimetype = $file->get_mimetype();
277         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
278             // make a preview of the image
279             $data = $this->create_imagefile_preview($file, $mode);
281         } else {
282             // unable to create the preview of this mimetype yet
283             return false;
284         }
286         if (empty($data)) {
287             return false;
288         }
290         // getimagesizefromstring() is available from PHP 5.4 but we need to support
291         // lower versions, so...
292         $tmproot = make_temp_directory('thumbnails');
293         $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
294         file_put_contents($tmpfilepath, $data);
295         $imageinfo = getimagesize($tmpfilepath);
296         unlink($tmpfilepath);
298         $context = context_system::instance();
300         $record = array(
301             'contextid' => $context->id,
302             'component' => 'core',
303             'filearea'  => 'preview',
304             'itemid'    => 0,
305             'filepath'  => '/' . trim($mode, '/') . '/',
306             'filename'  => $file->get_contenthash(),
307         );
309         if ($imageinfo) {
310             $record['mimetype'] = $imageinfo['mime'];
311         }
313         return $this->create_file_from_string($record, $data);
314     }
316     /**
317      * Generates a preview for the stored image file
318      *
319      * @param stored_file $file the image we want to preview
320      * @param string $mode preview mode, eg. 'thumb'
321      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
322      */
323     protected function create_imagefile_preview(stored_file $file, $mode) {
324         global $CFG;
325         require_once($CFG->libdir.'/gdlib.php');
327         $tmproot = make_temp_directory('thumbnails');
328         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
329         $file->copy_content_to($tmpfilepath);
331         if ($mode === 'tinyicon') {
332             $data = generate_image_thumbnail($tmpfilepath, 24, 24);
334         } else if ($mode === 'thumb') {
335             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
337         } else {
338             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
339         }
341         unlink($tmpfilepath);
343         return $data;
344     }
346     /**
347      * Fetch file using local file id.
348      *
349      * Please do not rely on file ids, it is usually easier to use
350      * pathname hashes instead.
351      *
352      * @param int $fileid file ID
353      * @return stored_file|bool stored_file instance if exists, false if not
354      */
355     public function get_file_by_id($fileid) {
356         global $DB;
358         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
359                   FROM {files} f
360              LEFT JOIN {files_reference} r
361                        ON f.referencefileid = r.id
362                  WHERE f.id = ?";
363         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
364             return $this->get_file_instance($filerecord);
365         } else {
366             return false;
367         }
368     }
370     /**
371      * Fetch file using local file full pathname hash
372      *
373      * @param string $pathnamehash path name hash
374      * @return stored_file|bool stored_file instance if exists, false if not
375      */
376     public function get_file_by_hash($pathnamehash) {
377         global $DB;
379         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
380                   FROM {files} f
381              LEFT JOIN {files_reference} r
382                        ON f.referencefileid = r.id
383                  WHERE f.pathnamehash = ?";
384         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
385             return $this->get_file_instance($filerecord);
386         } else {
387             return false;
388         }
389     }
391     /**
392      * Fetch locally stored file.
393      *
394      * @param int $contextid context ID
395      * @param string $component component
396      * @param string $filearea file area
397      * @param int $itemid item ID
398      * @param string $filepath file path
399      * @param string $filename file name
400      * @return stored_file|bool stored_file instance if exists, false if not
401      */
402     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
403         $filepath = clean_param($filepath, PARAM_PATH);
404         $filename = clean_param($filename, PARAM_FILE);
406         if ($filename === '') {
407             $filename = '.';
408         }
410         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
411         return $this->get_file_by_hash($pathnamehash);
412     }
414     /**
415      * Are there any files (or directories)
416      *
417      * @param int $contextid context ID
418      * @param string $component component
419      * @param string $filearea file area
420      * @param bool|int $itemid item id or false if all items
421      * @param bool $ignoredirs whether or not ignore directories
422      * @return bool empty
423      */
424     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
425         global $DB;
427         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
428         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
430         if ($itemid !== false) {
431             $params['itemid'] = $itemid;
432             $where .= " AND itemid = :itemid";
433         }
435         if ($ignoredirs) {
436             $sql = "SELECT 'x'
437                       FROM {files}
438                      WHERE $where AND filename <> '.'";
439         } else {
440             $sql = "SELECT 'x'
441                       FROM {files}
442                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
443         }
445         return !$DB->record_exists_sql($sql, $params);
446     }
448     /**
449      * Returns all files belonging to given repository
450      *
451      * @param int $repositoryid
452      * @param string $sort A fragment of SQL to use for sorting
453      */
454     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
455         global $DB;
456         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
457                   FROM {files} f
458              LEFT JOIN {files_reference} r
459                        ON f.referencefileid = r.id
460                  WHERE r.repositoryid = ?";
461         if (!empty($sort)) {
462             $sql .= " ORDER BY {$sort}";
463         }
465         $result = array();
466         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
467         foreach ($filerecords as $filerecord) {
468             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
469         }
470         return $result;
471     }
473     /**
474      * Returns all area files (optionally limited by itemid)
475      *
476      * @param int $contextid context ID
477      * @param string $component component
478      * @param string $filearea file area
479      * @param int $itemid item ID or all files if not specified
480      * @param string $sort A fragment of SQL to use for sorting
481      * @param bool $includedirs whether or not include directories
482      * @return array of stored_files indexed by pathanmehash
483      */
484     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
485         global $DB;
487         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
488         if ($itemid !== false) {
489             $itemidsql = ' AND f.itemid = :itemid ';
490             $conditions['itemid'] = $itemid;
491         } else {
492             $itemidsql = '';
493         }
495         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
496                   FROM {files} f
497              LEFT JOIN {files_reference} r
498                        ON f.referencefileid = r.id
499                  WHERE f.contextid = :contextid
500                        AND f.component = :component
501                        AND f.filearea = :filearea
502                        $itemidsql";
503         if (!empty($sort)) {
504             $sql .= " ORDER BY {$sort}";
505         }
507         $result = array();
508         $filerecords = $DB->get_records_sql($sql, $conditions);
509         foreach ($filerecords as $filerecord) {
510             if (!$includedirs and $filerecord->filename === '.') {
511                 continue;
512             }
513             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
514         }
515         return $result;
516     }
518     /**
519      * Returns array based tree structure of area files
520      *
521      * @param int $contextid context ID
522      * @param string $component component
523      * @param string $filearea file area
524      * @param int $itemid item ID
525      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
526      */
527     public function get_area_tree($contextid, $component, $filearea, $itemid) {
528         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
529         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
530         // first create directory structure
531         foreach ($files as $hash=>$dir) {
532             if (!$dir->is_directory()) {
533                 continue;
534             }
535             unset($files[$hash]);
536             if ($dir->get_filepath() === '/') {
537                 $result['dirfile'] = $dir;
538                 continue;
539             }
540             $parts = explode('/', trim($dir->get_filepath(),'/'));
541             $pointer =& $result;
542             foreach ($parts as $part) {
543                 if ($part === '') {
544                     continue;
545                 }
546                 if (!isset($pointer['subdirs'][$part])) {
547                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
548                 }
549                 $pointer =& $pointer['subdirs'][$part];
550             }
551             $pointer['dirfile'] = $dir;
552             unset($pointer);
553         }
554         foreach ($files as $hash=>$file) {
555             $parts = explode('/', trim($file->get_filepath(),'/'));
556             $pointer =& $result;
557             foreach ($parts as $part) {
558                 if ($part === '') {
559                     continue;
560                 }
561                 $pointer =& $pointer['subdirs'][$part];
562             }
563             $pointer['files'][$file->get_filename()] = $file;
564             unset($pointer);
565         }
566         $result = $this->sort_area_tree($result);
567         return $result;
568     }
570     /**
571      * Sorts the result of {@link file_storage::get_area_tree()}.
572      *
573      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
574      * @return array of sorted results
575      */
576     protected function sort_area_tree($tree) {
577         foreach ($tree as $key => &$value) {
578             if ($key == 'subdirs') {
579                 $value = $this->sort_area_tree($value);
580                 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
581             } else if ($key == 'files') {
582                 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
583             }
584         }
585         return $tree;
586     }
588     /**
589      * Returns all files and optionally directories
590      *
591      * @param int $contextid context ID
592      * @param string $component component
593      * @param string $filearea file area
594      * @param int $itemid item ID
595      * @param int $filepath directory path
596      * @param bool $recursive include all subdirectories
597      * @param bool $includedirs include files and directories
598      * @param string $sort A fragment of SQL to use for sorting
599      * @return array of stored_files indexed by pathanmehash
600      */
601     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
602         global $DB;
604         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
605             return array();
606         }
608         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
610         if ($recursive) {
612             $dirs = $includedirs ? "" : "AND filename <> '.'";
613             $length = textlib::strlen($filepath);
615             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
616                       FROM {files} f
617                  LEFT JOIN {files_reference} r
618                            ON f.referencefileid = r.id
619                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
620                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
621                            AND f.id <> :dirid
622                            $dirs
623                            $orderby";
624             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
626             $files = array();
627             $dirs  = array();
628             $filerecords = $DB->get_records_sql($sql, $params);
629             foreach ($filerecords as $filerecord) {
630                 if ($filerecord->filename == '.') {
631                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
632                 } else {
633                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
634                 }
635             }
636             $result = array_merge($dirs, $files);
638         } else {
639             $result = array();
640             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
642             $length = textlib::strlen($filepath);
644             if ($includedirs) {
645                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
646                           FROM {files} f
647                      LEFT JOIN {files_reference} r
648                                ON f.referencefileid = r.id
649                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
650                                AND f.itemid = :itemid AND f.filename = '.'
651                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
652                                AND f.id <> :dirid
653                                $orderby";
654                 $reqlevel = substr_count($filepath, '/') + 1;
655                 $filerecords = $DB->get_records_sql($sql, $params);
656                 foreach ($filerecords as $filerecord) {
657                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
658                         continue;
659                     }
660                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
661                 }
662             }
664             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
665                       FROM {files} f
666                  LEFT JOIN {files_reference} r
667                            ON f.referencefileid = r.id
668                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
669                            AND f.filepath = :filepath AND f.filename <> '.'
670                            $orderby";
672             $filerecords = $DB->get_records_sql($sql, $params);
673             foreach ($filerecords as $filerecord) {
674                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
675             }
676         }
678         return $result;
679     }
681     /**
682      * Delete all area files (optionally limited by itemid).
683      *
684      * @param int $contextid context ID
685      * @param string $component component
686      * @param string $filearea file area or all areas in context if not specified
687      * @param int $itemid item ID or all files if not specified
688      * @return bool success
689      */
690     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
691         global $DB;
693         $conditions = array('contextid'=>$contextid);
694         if ($component !== false) {
695             $conditions['component'] = $component;
696         }
697         if ($filearea !== false) {
698             $conditions['filearea'] = $filearea;
699         }
700         if ($itemid !== false) {
701             $conditions['itemid'] = $itemid;
702         }
704         $filerecords = $DB->get_records('files', $conditions);
705         foreach ($filerecords as $filerecord) {
706             $this->get_file_instance($filerecord)->delete();
707         }
709         return true; // BC only
710     }
712     /**
713      * Delete all the files from certain areas where itemid is limited by an
714      * arbitrary bit of SQL.
715      *
716      * @param int $contextid the id of the context the files belong to. Must be given.
717      * @param string $component the owning component. Must be given.
718      * @param string $filearea the file area name. Must be given.
719      * @param string $itemidstest an SQL fragment that the itemid must match. Used
720      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
721      *      and may not used named parameters called contextid, component or filearea.
722      * @param array $params any query params used by $itemidstest.
723      */
724     public function delete_area_files_select($contextid, $component,
725             $filearea, $itemidstest, array $params = null) {
726         global $DB;
728         $where = "contextid = :contextid
729                 AND component = :component
730                 AND filearea = :filearea
731                 AND itemid $itemidstest";
732         $params['contextid'] = $contextid;
733         $params['component'] = $component;
734         $params['filearea'] = $filearea;
736         $filerecords = $DB->get_recordset_select('files', $where, $params);
737         foreach ($filerecords as $filerecord) {
738             $this->get_file_instance($filerecord)->delete();
739         }
740         $filerecords->close();
741     }
743     /**
744      * Move all the files in a file area from one context to another.
745      *
746      * @param int $oldcontextid the context the files are being moved from.
747      * @param int $newcontextid the context the files are being moved to.
748      * @param string $component the plugin that these files belong to.
749      * @param string $filearea the name of the file area.
750      * @param int $itemid file item ID
751      * @return int the number of files moved, for information.
752      */
753     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
754         // Note, this code is based on some code that Petr wrote in
755         // forum_move_attachments in mod/forum/lib.php. I moved it here because
756         // I needed it in the question code too.
757         $count = 0;
759         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
760         foreach ($oldfiles as $oldfile) {
761             $filerecord = new stdClass();
762             $filerecord->contextid = $newcontextid;
763             $this->create_file_from_storedfile($filerecord, $oldfile);
764             $count += 1;
765         }
767         if ($count) {
768             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
769         }
771         return $count;
772     }
774     /**
775      * Recursively creates directory.
776      *
777      * @param int $contextid context ID
778      * @param string $component component
779      * @param string $filearea file area
780      * @param int $itemid item ID
781      * @param string $filepath file path
782      * @param int $userid the user ID
783      * @return bool success
784      */
785     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
786         global $DB;
788         // validate all parameters, we do not want any rubbish stored in database, right?
789         if (!is_number($contextid) or $contextid < 1) {
790             throw new file_exception('storedfileproblem', 'Invalid contextid');
791         }
793         $component = clean_param($component, PARAM_COMPONENT);
794         if (empty($component)) {
795             throw new file_exception('storedfileproblem', 'Invalid component');
796         }
798         $filearea = clean_param($filearea, PARAM_AREA);
799         if (empty($filearea)) {
800             throw new file_exception('storedfileproblem', 'Invalid filearea');
801         }
803         if (!is_number($itemid) or $itemid < 0) {
804             throw new file_exception('storedfileproblem', 'Invalid itemid');
805         }
807         $filepath = clean_param($filepath, PARAM_PATH);
808         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
809             // path must start and end with '/'
810             throw new file_exception('storedfileproblem', 'Invalid file path');
811         }
813         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
815         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
816             return $dir_info;
817         }
819         static $contenthash = null;
820         if (!$contenthash) {
821             $this->add_string_to_pool('');
822             $contenthash = sha1('');
823         }
825         $now = time();
827         $dir_record = new stdClass();
828         $dir_record->contextid = $contextid;
829         $dir_record->component = $component;
830         $dir_record->filearea  = $filearea;
831         $dir_record->itemid    = $itemid;
832         $dir_record->filepath  = $filepath;
833         $dir_record->filename  = '.';
834         $dir_record->contenthash  = $contenthash;
835         $dir_record->filesize  = 0;
837         $dir_record->timecreated  = $now;
838         $dir_record->timemodified = $now;
839         $dir_record->mimetype     = null;
840         $dir_record->userid       = $userid;
842         $dir_record->pathnamehash = $pathnamehash;
844         $DB->insert_record('files', $dir_record);
845         $dir_info = $this->get_file_by_hash($pathnamehash);
847         if ($filepath !== '/') {
848             //recurse to parent dirs
849             $filepath = trim($filepath, '/');
850             $filepath = explode('/', $filepath);
851             array_pop($filepath);
852             $filepath = implode('/', $filepath);
853             $filepath = ($filepath === '') ? '/' : "/$filepath/";
854             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
855         }
857         return $dir_info;
858     }
860     /**
861      * Add new local file based on existing local file.
862      *
863      * @param stdClass|array $filerecord object or array describing changes
864      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
865      * @return stored_file instance of newly created file
866      */
867     public function create_file_from_storedfile($filerecord, $fileorid) {
868         global $DB;
870         if ($fileorid instanceof stored_file) {
871             $fid = $fileorid->get_id();
872         } else {
873             $fid = $fileorid;
874         }
876         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
878         unset($filerecord['id']);
879         unset($filerecord['filesize']);
880         unset($filerecord['contenthash']);
881         unset($filerecord['pathnamehash']);
883         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
884                   FROM {files} f
885              LEFT JOIN {files_reference} r
886                        ON f.referencefileid = r.id
887                  WHERE f.id = ?";
889         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
890             throw new file_exception('storedfileproblem', 'File does not exist');
891         }
893         unset($newrecord->id);
895         foreach ($filerecord as $key => $value) {
896             // validate all parameters, we do not want any rubbish stored in database, right?
897             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
898                 throw new file_exception('storedfileproblem', 'Invalid contextid');
899             }
901             if ($key == 'component') {
902                 $value = clean_param($value, PARAM_COMPONENT);
903                 if (empty($value)) {
904                     throw new file_exception('storedfileproblem', 'Invalid component');
905                 }
906             }
908             if ($key == 'filearea') {
909                 $value = clean_param($value, PARAM_AREA);
910                 if (empty($value)) {
911                     throw new file_exception('storedfileproblem', 'Invalid filearea');
912                 }
913             }
915             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
916                 throw new file_exception('storedfileproblem', 'Invalid itemid');
917             }
920             if ($key == 'filepath') {
921                 $value = clean_param($value, PARAM_PATH);
922                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
923                     // path must start and end with '/'
924                     throw new file_exception('storedfileproblem', 'Invalid file path');
925                 }
926             }
928             if ($key == 'filename') {
929                 $value = clean_param($value, PARAM_FILE);
930                 if ($value === '') {
931                     // path must start and end with '/'
932                     throw new file_exception('storedfileproblem', 'Invalid file name');
933                 }
934             }
936             if ($key === 'timecreated' or $key === 'timemodified') {
937                 if (!is_number($value)) {
938                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
939                 }
940                 if ($value < 0) {
941                     //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)
942                     $value = 0;
943                 }
944             }
946             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
947                 $value = clean_param($value, PARAM_INT);
948             }
950             $newrecord->$key = $value;
951         }
953         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
955         if ($newrecord->filename === '.') {
956             // special case - only this function supports directories ;-)
957             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
958             // update the existing directory with the new data
959             $newrecord->id = $directory->get_id();
960             $DB->update_record('files', $newrecord);
961             return $this->get_file_instance($newrecord);
962         }
964         // note: referencefileid is copied from the original file so that
965         // creating a new file from an existing alias creates new alias implicitly.
966         // here we just check the database consistency.
967         if (!empty($newrecord->repositoryid)) {
968             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
969                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
970             }
971         }
973         try {
974             $newrecord->id = $DB->insert_record('files', $newrecord);
975         } catch (dml_exception $e) {
976             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
977                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
978         }
981         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
983         return $this->get_file_instance($newrecord);
984     }
986     /**
987      * Add new local file.
988      *
989      * @param stdClass|array $filerecord object or array describing file
990      * @param string $url the URL to the file
991      * @param array $options {@link download_file_content()} options
992      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
993      * @return stored_file
994      */
995     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
997         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
998         $filerecord = (object)$filerecord; // We support arrays too.
1000         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1001         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1002         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1003         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1004         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1005         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1006         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1008         if (!isset($filerecord->filename)) {
1009             $parts = explode('/', $url);
1010             $filename = array_pop($parts);
1011             $filerecord->filename = clean_param($filename, PARAM_FILE);
1012         }
1013         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1014         $filerecord->source = clean_param($source, PARAM_URL);
1016         if ($usetempfile) {
1017             check_dir_exists($this->tempdir);
1018             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1019             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1020             if ($content === false) {
1021                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1022             }
1023             try {
1024                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1025                 @unlink($tmpfile);
1026                 return $newfile;
1027             } catch (Exception $e) {
1028                 @unlink($tmpfile);
1029                 throw $e;
1030             }
1032         } else {
1033             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1034             if ($content === false) {
1035                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1036             }
1037             return $this->create_file_from_string($filerecord, $content);
1038         }
1039     }
1041     /**
1042      * Add new local file.
1043      *
1044      * @param stdClass|array $filerecord object or array describing file
1045      * @param string $pathname path to file or content of file
1046      * @return stored_file
1047      */
1048     public function create_file_from_pathname($filerecord, $pathname) {
1049         global $DB;
1051         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1052         $filerecord = (object)$filerecord; // We support arrays too.
1054         // validate all parameters, we do not want any rubbish stored in database, right?
1055         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1056             throw new file_exception('storedfileproblem', 'Invalid contextid');
1057         }
1059         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1060         if (empty($filerecord->component)) {
1061             throw new file_exception('storedfileproblem', 'Invalid component');
1062         }
1064         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1065         if (empty($filerecord->filearea)) {
1066             throw new file_exception('storedfileproblem', 'Invalid filearea');
1067         }
1069         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1070             throw new file_exception('storedfileproblem', 'Invalid itemid');
1071         }
1073         if (!empty($filerecord->sortorder)) {
1074             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1075                 $filerecord->sortorder = 0;
1076             }
1077         } else {
1078             $filerecord->sortorder = 0;
1079         }
1081         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1082         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1083             // path must start and end with '/'
1084             throw new file_exception('storedfileproblem', 'Invalid file path');
1085         }
1087         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1088         if ($filerecord->filename === '') {
1089             // filename must not be empty
1090             throw new file_exception('storedfileproblem', 'Invalid file name');
1091         }
1093         $now = time();
1094         if (isset($filerecord->timecreated)) {
1095             if (!is_number($filerecord->timecreated)) {
1096                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1097             }
1098             if ($filerecord->timecreated < 0) {
1099                 //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)
1100                 $filerecord->timecreated = 0;
1101             }
1102         } else {
1103             $filerecord->timecreated = $now;
1104         }
1106         if (isset($filerecord->timemodified)) {
1107             if (!is_number($filerecord->timemodified)) {
1108                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1109             }
1110             if ($filerecord->timemodified < 0) {
1111                 //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)
1112                 $filerecord->timemodified = 0;
1113             }
1114         } else {
1115             $filerecord->timemodified = $now;
1116         }
1118         $newrecord = new stdClass();
1120         $newrecord->contextid = $filerecord->contextid;
1121         $newrecord->component = $filerecord->component;
1122         $newrecord->filearea  = $filerecord->filearea;
1123         $newrecord->itemid    = $filerecord->itemid;
1124         $newrecord->filepath  = $filerecord->filepath;
1125         $newrecord->filename  = $filerecord->filename;
1127         $newrecord->timecreated  = $filerecord->timecreated;
1128         $newrecord->timemodified = $filerecord->timemodified;
1129         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1130         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1131         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1132         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1133         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1134         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1135         $newrecord->sortorder    = $filerecord->sortorder;
1137         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1139         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1141         try {
1142             $newrecord->id = $DB->insert_record('files', $newrecord);
1143         } catch (dml_exception $e) {
1144             if ($newfile) {
1145                 $this->deleted_file_cleanup($newrecord->contenthash);
1146             }
1147             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1148                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1149         }
1151         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1153         return $this->get_file_instance($newrecord);
1154     }
1156     /**
1157      * Add new local file.
1158      *
1159      * @param stdClass|array $filerecord object or array describing file
1160      * @param string $content content of file
1161      * @return stored_file
1162      */
1163     public function create_file_from_string($filerecord, $content) {
1164         global $DB;
1166         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1167         $filerecord = (object)$filerecord; // We support arrays too.
1169         // validate all parameters, we do not want any rubbish stored in database, right?
1170         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1171             throw new file_exception('storedfileproblem', 'Invalid contextid');
1172         }
1174         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1175         if (empty($filerecord->component)) {
1176             throw new file_exception('storedfileproblem', 'Invalid component');
1177         }
1179         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1180         if (empty($filerecord->filearea)) {
1181             throw new file_exception('storedfileproblem', 'Invalid filearea');
1182         }
1184         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1185             throw new file_exception('storedfileproblem', 'Invalid itemid');
1186         }
1188         if (!empty($filerecord->sortorder)) {
1189             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1190                 $filerecord->sortorder = 0;
1191             }
1192         } else {
1193             $filerecord->sortorder = 0;
1194         }
1196         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1197         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1198             // path must start and end with '/'
1199             throw new file_exception('storedfileproblem', 'Invalid file path');
1200         }
1202         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1203         if ($filerecord->filename === '') {
1204             // path must start and end with '/'
1205             throw new file_exception('storedfileproblem', 'Invalid file name');
1206         }
1208         $now = time();
1209         if (isset($filerecord->timecreated)) {
1210             if (!is_number($filerecord->timecreated)) {
1211                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1212             }
1213             if ($filerecord->timecreated < 0) {
1214                 //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)
1215                 $filerecord->timecreated = 0;
1216             }
1217         } else {
1218             $filerecord->timecreated = $now;
1219         }
1221         if (isset($filerecord->timemodified)) {
1222             if (!is_number($filerecord->timemodified)) {
1223                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1224             }
1225             if ($filerecord->timemodified < 0) {
1226                 //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)
1227                 $filerecord->timemodified = 0;
1228             }
1229         } else {
1230             $filerecord->timemodified = $now;
1231         }
1233         $newrecord = new stdClass();
1235         $newrecord->contextid = $filerecord->contextid;
1236         $newrecord->component = $filerecord->component;
1237         $newrecord->filearea  = $filerecord->filearea;
1238         $newrecord->itemid    = $filerecord->itemid;
1239         $newrecord->filepath  = $filerecord->filepath;
1240         $newrecord->filename  = $filerecord->filename;
1242         $newrecord->timecreated  = $filerecord->timecreated;
1243         $newrecord->timemodified = $filerecord->timemodified;
1244         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1245         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1246         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1247         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1248         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1249         $newrecord->sortorder    = $filerecord->sortorder;
1251         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1252         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1253         // get mimetype by magic bytes
1254         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1256         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1258         try {
1259             $newrecord->id = $DB->insert_record('files', $newrecord);
1260         } catch (dml_exception $e) {
1261             if ($newfile) {
1262                 $this->deleted_file_cleanup($newrecord->contenthash);
1263             }
1264             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1265                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1266         }
1268         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1270         return $this->get_file_instance($newrecord);
1271     }
1273     /**
1274      * Create a new alias/shortcut file from file reference information
1275      *
1276      * @param stdClass|array $filerecord object or array describing the new file
1277      * @param int $repositoryid the id of the repository that provides the original file
1278      * @param string $reference the information required by the repository to locate the original file
1279      * @param array $options options for creating the new file
1280      * @return stored_file
1281      */
1282     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1283         global $DB;
1285         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1286         $filerecord = (object)$filerecord; // We support arrays too.
1288         // validate all parameters, we do not want any rubbish stored in database, right?
1289         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1290             throw new file_exception('storedfileproblem', 'Invalid contextid');
1291         }
1293         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1294         if (empty($filerecord->component)) {
1295             throw new file_exception('storedfileproblem', 'Invalid component');
1296         }
1298         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1299         if (empty($filerecord->filearea)) {
1300             throw new file_exception('storedfileproblem', 'Invalid filearea');
1301         }
1303         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1304             throw new file_exception('storedfileproblem', 'Invalid itemid');
1305         }
1307         if (!empty($filerecord->sortorder)) {
1308             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1309                 $filerecord->sortorder = 0;
1310             }
1311         } else {
1312             $filerecord->sortorder = 0;
1313         }
1315         // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
1316         unset($filerecord->referencelastsync);
1317         unset($filerecord->referencelifetime);
1319         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1320         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1321         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1322         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1323         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1324         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1325         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1326         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1327             // Path must start and end with '/'.
1328             throw new file_exception('storedfileproblem', 'Invalid file path');
1329         }
1331         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1332         if ($filerecord->filename === '') {
1333             // Path must start and end with '/'.
1334             throw new file_exception('storedfileproblem', 'Invalid file name');
1335         }
1337         $now = time();
1338         if (isset($filerecord->timecreated)) {
1339             if (!is_number($filerecord->timecreated)) {
1340                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1341             }
1342             if ($filerecord->timecreated < 0) {
1343                 // 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)
1344                 $filerecord->timecreated = 0;
1345             }
1346         } else {
1347             $filerecord->timecreated = $now;
1348         }
1350         if (isset($filerecord->timemodified)) {
1351             if (!is_number($filerecord->timemodified)) {
1352                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1353             }
1354             if ($filerecord->timemodified < 0) {
1355                 // 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)
1356                 $filerecord->timemodified = 0;
1357             }
1358         } else {
1359             $filerecord->timemodified = $now;
1360         }
1362         $transaction = $DB->start_delegated_transaction();
1364         try {
1365             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1366         } catch (Exception $e) {
1367             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1368         }
1370         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1371             // there was specified the contenthash for a file already stored in moodle filepool
1372             if (empty($filerecord->filesize)) {
1373                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1374                 $filerecord->filesize = filesize($filepathname);
1375             } else {
1376                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1377             }
1378         } else {
1379             // atempt to get the result of last synchronisation for this reference
1380             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1381                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1382             if ($lastcontent) {
1383                 $filerecord->contenthash = $lastcontent->contenthash;
1384                 $filerecord->filesize = $lastcontent->filesize;
1385             } else {
1386                 // External file doesn't have content in moodle.
1387                 // So we create an empty file for it.
1388                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1389             }
1390         }
1392         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1394         try {
1395             $filerecord->id = $DB->insert_record('files', $filerecord);
1396         } catch (dml_exception $e) {
1397             if (!empty($newfile)) {
1398                 $this->deleted_file_cleanup($filerecord->contenthash);
1399             }
1400             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1401                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1402         }
1404         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1406         $transaction->allow_commit();
1408         // this will retrieve all reference information from DB as well
1409         return $this->get_file_by_id($filerecord->id);
1410     }
1412     /**
1413      * Creates new image file from existing.
1414      *
1415      * @param stdClass|array $filerecord object or array describing new file
1416      * @param int|stored_file $fid file id or stored file object
1417      * @param int $newwidth in pixels
1418      * @param int $newheight in pixels
1419      * @param bool $keepaspectratio whether or not keep aspect ratio
1420      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1421      * @return stored_file
1422      */
1423     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1424         if (!function_exists('imagecreatefromstring')) {
1425             //Most likely the GD php extension isn't installed
1426             //image conversion cannot succeed
1427             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1428         }
1430         if ($fid instanceof stored_file) {
1431             $fid = $fid->get_id();
1432         }
1434         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1436         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1437             throw new file_exception('storedfileproblem', 'File does not exist');
1438         }
1440         if (!$imageinfo = $file->get_imageinfo()) {
1441             throw new file_exception('storedfileproblem', 'File is not an image');
1442         }
1444         if (!isset($filerecord['filename'])) {
1445             $filerecord['filename'] = $file->get_filename();
1446         }
1448         if (!isset($filerecord['mimetype'])) {
1449             $filerecord['mimetype'] = $imageinfo['mimetype'];
1450         }
1452         $width    = $imageinfo['width'];
1453         $height   = $imageinfo['height'];
1454         $mimetype = $imageinfo['mimetype'];
1456         if ($keepaspectratio) {
1457             if (0 >= $newwidth and 0 >= $newheight) {
1458                 // no sizes specified
1459                 $newwidth  = $width;
1460                 $newheight = $height;
1462             } else if (0 < $newwidth and 0 < $newheight) {
1463                 $xheight = ($newwidth*($height/$width));
1464                 if ($xheight < $newheight) {
1465                     $newheight = (int)$xheight;
1466                 } else {
1467                     $newwidth = (int)($newheight*($width/$height));
1468                 }
1470             } else if (0 < $newwidth) {
1471                 $newheight = (int)($newwidth*($height/$width));
1473             } else { //0 < $newheight
1474                 $newwidth = (int)($newheight*($width/$height));
1475             }
1477         } else {
1478             if (0 >= $newwidth) {
1479                 $newwidth = $width;
1480             }
1481             if (0 >= $newheight) {
1482                 $newheight = $height;
1483             }
1484         }
1486         $img = imagecreatefromstring($file->get_content());
1487         if ($height != $newheight or $width != $newwidth) {
1488             $newimg = imagecreatetruecolor($newwidth, $newheight);
1489             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1490                 // weird
1491                 throw new file_exception('storedfileproblem', 'Can not resize image');
1492             }
1493             imagedestroy($img);
1494             $img = $newimg;
1495         }
1497         ob_start();
1498         switch ($filerecord['mimetype']) {
1499             case 'image/gif':
1500                 imagegif($img);
1501                 break;
1503             case 'image/jpeg':
1504                 if (is_null($quality)) {
1505                     imagejpeg($img);
1506                 } else {
1507                     imagejpeg($img, NULL, $quality);
1508                 }
1509                 break;
1511             case 'image/png':
1512                 $quality = (int)$quality;
1513                 imagepng($img, NULL, $quality, NULL);
1514                 break;
1516             default:
1517                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1518         }
1520         $content = ob_get_contents();
1521         ob_end_clean();
1522         imagedestroy($img);
1524         if (!$content) {
1525             throw new file_exception('storedfileproblem', 'Can not convert image');
1526         }
1528         return $this->create_file_from_string($filerecord, $content);
1529     }
1531     /**
1532      * Add file content to sha1 pool.
1533      *
1534      * @param string $pathname path to file
1535      * @param string $contenthash sha1 hash of content if known (performance only)
1536      * @return array (contenthash, filesize, newfile)
1537      */
1538     public function add_file_to_pool($pathname, $contenthash = NULL) {
1539         if (!is_readable($pathname)) {
1540             throw new file_exception('storedfilecannotread', '', $pathname);
1541         }
1543         if (is_null($contenthash)) {
1544             $contenthash = sha1_file($pathname);
1545         }
1547         $filesize = filesize($pathname);
1549         $hashpath = $this->path_from_hash($contenthash);
1550         $hashfile = "$hashpath/$contenthash";
1552         if (file_exists($hashfile)) {
1553             if (filesize($hashfile) !== $filesize) {
1554                 throw new file_pool_content_exception($contenthash);
1555             }
1556             $newfile = false;
1558         } else {
1559             if (!is_dir($hashpath)) {
1560                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1561                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1562                 }
1563             }
1564             $newfile = true;
1566             if (!copy($pathname, $hashfile)) {
1567                 throw new file_exception('storedfilecannotread', '', $pathname);
1568             }
1570             if (filesize($hashfile) !== $filesize) {
1571                 @unlink($hashfile);
1572                 throw new file_pool_content_exception($contenthash);
1573             }
1574             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1575         }
1578         return array($contenthash, $filesize, $newfile);
1579     }
1581     /**
1582      * Add string content to sha1 pool.
1583      *
1584      * @param string $content file content - binary string
1585      * @return array (contenthash, filesize, newfile)
1586      */
1587     public function add_string_to_pool($content) {
1588         $contenthash = sha1($content);
1589         $filesize = strlen($content); // binary length
1591         $hashpath = $this->path_from_hash($contenthash);
1592         $hashfile = "$hashpath/$contenthash";
1595         if (file_exists($hashfile)) {
1596             if (filesize($hashfile) !== $filesize) {
1597                 throw new file_pool_content_exception($contenthash);
1598             }
1599             $newfile = false;
1601         } else {
1602             if (!is_dir($hashpath)) {
1603                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1604                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1605                 }
1606             }
1607             $newfile = true;
1609             file_put_contents($hashfile, $content);
1611             if (filesize($hashfile) !== $filesize) {
1612                 @unlink($hashfile);
1613                 throw new file_pool_content_exception($contenthash);
1614             }
1615             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1616         }
1618         return array($contenthash, $filesize, $newfile);
1619     }
1621     /**
1622      * Serve file content using X-Sendfile header.
1623      * Please make sure that all headers are already sent
1624      * and the all access control checks passed.
1625      *
1626      * @param string $contenthash sah1 hash of the file content to be served
1627      * @return bool success
1628      */
1629     public function xsendfile($contenthash) {
1630         global $CFG;
1631         require_once("$CFG->libdir/xsendfilelib.php");
1633         $hashpath = $this->path_from_hash($contenthash);
1634         return xsendfile("$hashpath/$contenthash");
1635     }
1637     /**
1638      * Content exists
1639      *
1640      * @param string $contenthash
1641      * @return bool
1642      */
1643     public function content_exists($contenthash) {
1644         $dir = $this->path_from_hash($contenthash);
1645         $filepath = $dir . '/' . $contenthash;
1646         return file_exists($filepath);
1647     }
1649     /**
1650      * Return path to file with given hash.
1651      *
1652      * NOTE: must not be public, files in pool must not be modified
1653      *
1654      * @param string $contenthash content hash
1655      * @return string expected file location
1656      */
1657     protected function path_from_hash($contenthash) {
1658         $l1 = $contenthash[0].$contenthash[1];
1659         $l2 = $contenthash[2].$contenthash[3];
1660         return "$this->filedir/$l1/$l2";
1661     }
1663     /**
1664      * Return path to file with given hash.
1665      *
1666      * NOTE: must not be public, files in pool must not be modified
1667      *
1668      * @param string $contenthash content hash
1669      * @return string expected file location
1670      */
1671     protected function trash_path_from_hash($contenthash) {
1672         $l1 = $contenthash[0].$contenthash[1];
1673         $l2 = $contenthash[2].$contenthash[3];
1674         return "$this->trashdir/$l1/$l2";
1675     }
1677     /**
1678      * Tries to recover missing content of file from trash.
1679      *
1680      * @param stored_file $file stored_file instance
1681      * @return bool success
1682      */
1683     public function try_content_recovery($file) {
1684         $contenthash = $file->get_contenthash();
1685         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1686         if (!is_readable($trashfile)) {
1687             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1688                 return false;
1689             }
1690             // nice, at least alternative trash file in trash root exists
1691             $trashfile = $this->trashdir.'/'.$contenthash;
1692         }
1693         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1694             //weird, better fail early
1695             return false;
1696         }
1697         $contentdir  = $this->path_from_hash($contenthash);
1698         $contentfile = $contentdir.'/'.$contenthash;
1699         if (file_exists($contentfile)) {
1700             //strange, no need to recover anything
1701             return true;
1702         }
1703         if (!is_dir($contentdir)) {
1704             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1705                 return false;
1706             }
1707         }
1708         return rename($trashfile, $contentfile);
1709     }
1711     /**
1712      * Marks pool file as candidate for deleting.
1713      *
1714      * DO NOT call directly - reserved for core!!
1715      *
1716      * @param string $contenthash
1717      */
1718     public function deleted_file_cleanup($contenthash) {
1719         global $DB;
1721         //Note: this section is critical - in theory file could be reused at the same
1722         //      time, if this happens we can still recover the file from trash
1723         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1724             // file content is still used
1725             return;
1726         }
1727         //move content file to trash
1728         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1729         if (!file_exists($contentfile)) {
1730             //weird, but no problem
1731             return;
1732         }
1733         $trashpath = $this->trash_path_from_hash($contenthash);
1734         $trashfile = $trashpath.'/'.$contenthash;
1735         if (file_exists($trashfile)) {
1736             // we already have this content in trash, no need to move it there
1737             unlink($contentfile);
1738             return;
1739         }
1740         if (!is_dir($trashpath)) {
1741             mkdir($trashpath, $this->dirpermissions, true);
1742         }
1743         rename($contentfile, $trashfile);
1744         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1745     }
1747     /**
1748      * When user referring to a moodle file, we build the reference field
1749      *
1750      * @param array $params
1751      * @return string
1752      */
1753     public static function pack_reference($params) {
1754         $params = (array)$params;
1755         $reference = array();
1756         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1757         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1758         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1759         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1760         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1761         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1762         return base64_encode(serialize($reference));
1763     }
1765     /**
1766      * Unpack reference field
1767      *
1768      * @param string $str
1769      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1770      * @throws file_reference_exception if the $str does not have the expected format
1771      * @return array
1772      */
1773     public static function unpack_reference($str, $cleanparams = false) {
1774         $decoded = base64_decode($str, true);
1775         if ($decoded === false) {
1776             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1777         }
1778         $params = @unserialize($decoded); // hide E_NOTICE
1779         if ($params === false) {
1780             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1781         }
1782         if (is_array($params) && $cleanparams) {
1783             $params = array(
1784                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1785                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1786                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1787                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1788                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1789                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1790             );
1791         }
1792         return $params;
1793     }
1795     /**
1796      * Returns all aliases that refer to some stored_file via the given reference
1797      *
1798      * All repositories that provide access to a stored_file are expected to use
1799      * {@link self::pack_reference()}. This method can't be used if the given reference
1800      * does not use this format or if you are looking for references to an external file
1801      * (for example it can't be used to search for all aliases that refer to a given
1802      * Dropbox or Box.net file).
1803      *
1804      * Aliases in user draft areas are excluded from the returned list.
1805      *
1806      * @param string $reference identification of the referenced file
1807      * @return array of stored_file indexed by its pathnamehash
1808      */
1809     public function search_references($reference) {
1810         global $DB;
1812         if (is_null($reference)) {
1813             throw new coding_exception('NULL is not a valid reference to an external file');
1814         }
1816         // Give {@link self::unpack_reference()} a chance to throw exception if the
1817         // reference is not in a valid format.
1818         self::unpack_reference($reference);
1820         $referencehash = sha1($reference);
1822         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1823                   FROM {files} f
1824                   JOIN {files_reference} r ON f.referencefileid = r.id
1825                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1826                  WHERE r.referencehash = ?
1827                        AND (f.component <> ? OR f.filearea <> ?)";
1829         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1830         $files = array();
1831         foreach ($rs as $filerecord) {
1832             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1833         }
1835         return $files;
1836     }
1838     /**
1839      * Returns the number of aliases that refer to some stored_file via the given reference
1840      *
1841      * All repositories that provide access to a stored_file are expected to use
1842      * {@link self::pack_reference()}. This method can't be used if the given reference
1843      * does not use this format or if you are looking for references to an external file
1844      * (for example it can't be used to count aliases that refer to a given Dropbox or
1845      * Box.net file).
1846      *
1847      * Aliases in user draft areas are not counted.
1848      *
1849      * @param string $reference identification of the referenced file
1850      * @return int
1851      */
1852     public function search_references_count($reference) {
1853         global $DB;
1855         if (is_null($reference)) {
1856             throw new coding_exception('NULL is not a valid reference to an external file');
1857         }
1859         // Give {@link self::unpack_reference()} a chance to throw exception if the
1860         // reference is not in a valid format.
1861         self::unpack_reference($reference);
1863         $referencehash = sha1($reference);
1865         $sql = "SELECT COUNT(f.id)
1866                   FROM {files} f
1867                   JOIN {files_reference} r ON f.referencefileid = r.id
1868                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1869                  WHERE r.referencehash = ?
1870                        AND (f.component <> ? OR f.filearea <> ?)";
1872         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
1873     }
1875     /**
1876      * Returns all aliases that link to the given stored_file
1877      *
1878      * Aliases in user draft areas are excluded from the returned list.
1879      *
1880      * @param stored_file $storedfile
1881      * @return array of stored_file
1882      */
1883     public function get_references_by_storedfile(stored_file $storedfile) {
1884         global $DB;
1886         $params = array();
1887         $params['contextid'] = $storedfile->get_contextid();
1888         $params['component'] = $storedfile->get_component();
1889         $params['filearea']  = $storedfile->get_filearea();
1890         $params['itemid']    = $storedfile->get_itemid();
1891         $params['filename']  = $storedfile->get_filename();
1892         $params['filepath']  = $storedfile->get_filepath();
1894         return $this->search_references(self::pack_reference($params));
1895     }
1897     /**
1898      * Returns the number of aliases that link to the given stored_file
1899      *
1900      * Aliases in user draft areas are not counted.
1901      *
1902      * @param stored_file $storedfile
1903      * @return int
1904      */
1905     public function get_references_count_by_storedfile(stored_file $storedfile) {
1906         global $DB;
1908         $params = array();
1909         $params['contextid'] = $storedfile->get_contextid();
1910         $params['component'] = $storedfile->get_component();
1911         $params['filearea']  = $storedfile->get_filearea();
1912         $params['itemid']    = $storedfile->get_itemid();
1913         $params['filename']  = $storedfile->get_filename();
1914         $params['filepath']  = $storedfile->get_filepath();
1916         return $this->search_references_count(self::pack_reference($params));
1917     }
1919     /**
1920      * Updates all files that are referencing this file with the new contenthash
1921      * and filesize
1922      *
1923      * @param stored_file $storedfile
1924      */
1925     public function update_references_to_storedfile(stored_file $storedfile) {
1926         global $CFG, $DB;
1927         $params = array();
1928         $params['contextid'] = $storedfile->get_contextid();
1929         $params['component'] = $storedfile->get_component();
1930         $params['filearea']  = $storedfile->get_filearea();
1931         $params['itemid']    = $storedfile->get_itemid();
1932         $params['filename']  = $storedfile->get_filename();
1933         $params['filepath']  = $storedfile->get_filepath();
1934         $reference = self::pack_reference($params);
1935         $referencehash = sha1($reference);
1937         $sql = "SELECT repositoryid, id FROM {files_reference}
1938                  WHERE referencehash = ?";
1939         $rs = $DB->get_recordset_sql($sql, array($referencehash));
1941         $now = time();
1942         foreach ($rs as $record) {
1943             require_once($CFG->dirroot.'/repository/lib.php');
1944             $repo = repository::get_instance($record->repositoryid);
1945             $lifetime = $repo->get_reference_file_lifetime($reference);
1946             $this->update_references($record->id, $now, $lifetime,
1947                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
1948         }
1949         $rs->close();
1950     }
1952     /**
1953      * Convert file alias to local file
1954      *
1955      * @throws moodle_exception if file could not be downloaded
1956      *
1957      * @param stored_file $storedfile a stored_file instances
1958      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
1959      * @return stored_file stored_file
1960      */
1961     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
1962         global $CFG;
1963         $storedfile->import_external_file_contents($maxbytes);
1964         $storedfile->delete_reference();
1965         return $storedfile;
1966     }
1968     /**
1969      * Return mimetype by given file pathname
1970      *
1971      * If file has a known extension, we return the mimetype based on extension.
1972      * Otherwise (when possible) we try to get the mimetype from file contents.
1973      *
1974      * @param string $pathname full path to the file
1975      * @param string $filename correct file name with extension, if omitted will be taken from $path
1976      * @return string
1977      */
1978     public static function mimetype($pathname, $filename = null) {
1979         if (empty($filename)) {
1980             $filename = $pathname;
1981         }
1982         $type = mimeinfo('type', $filename);
1983         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1984             $finfo = new finfo(FILEINFO_MIME_TYPE);
1985             $type = mimeinfo_from_type('type', $finfo->file($pathname));
1986         }
1987         return $type;
1988     }
1990     /**
1991      * Cron cleanup job.
1992      */
1993     public function cron() {
1994         global $CFG, $DB;
1996         // find out all stale draft areas (older than 4 days) and purge them
1997         // those are identified by time stamp of the /. root dir
1998         mtrace('Deleting old draft files... ', '');
1999         $old = time() - 60*60*24*4;
2000         $sql = "SELECT *
2001                   FROM {files}
2002                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2003                        AND timecreated < :old";
2004         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2005         foreach ($rs as $dir) {
2006             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2007         }
2008         $rs->close();
2009         mtrace('done.');
2011         // remove orphaned preview files (that is files in the core preview filearea without
2012         // the existing original file)
2013         mtrace('Deleting orphaned preview files... ', '');
2014         $sql = "SELECT p.*
2015                   FROM {files} p
2016              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2017                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2018                        AND o.id IS NULL";
2019         $syscontext = context_system::instance();
2020         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2021         foreach ($rs as $orphan) {
2022             $file = $this->get_file_instance($orphan);
2023             if (!$file->is_directory()) {
2024                 $file->delete();
2025             }
2026         }
2027         $rs->close();
2028         mtrace('done.');
2030         // remove trash pool files once a day
2031         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2032         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2033             require_once($CFG->libdir.'/filelib.php');
2034             // Delete files that are associated with a context that no longer exists.
2035             mtrace('Cleaning up files from deleted contexts... ', '');
2036             $sql = "SELECT DISTINCT f.contextid
2037                     FROM {files} f
2038                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2039                     WHERE c.id IS NULL";
2040             $rs = $DB->get_recordset_sql($sql);
2041             if ($rs->valid()) {
2042                 $fs = get_file_storage();
2043                 foreach ($rs as $ctx) {
2044                     $fs->delete_area_files($ctx->contextid);
2045                 }
2046             }
2047             $rs->close();
2048             mtrace('done.');
2050             mtrace('Deleting trash files... ', '');
2051             fulldelete($this->trashdir);
2052             set_config('fileslastcleanup', time());
2053             mtrace('done.');
2054         }
2055     }
2057     /**
2058      * Get the sql formated fields for a file instance to be created from a
2059      * {files} and {files_refernece} join.
2060      *
2061      * @param string $filesprefix the table prefix for the {files} table
2062      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2063      * @return string the sql to go after a SELECT
2064      */
2065     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2066         // Note, these fieldnames MUST NOT overlap between the two tables,
2067         // else problems like MDL-33172 occur.
2068         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2069             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2070             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2072         $referencefields = array('repositoryid' => 'repositoryid',
2073             'reference' => 'reference',
2074             'lastsync' => 'referencelastsync',
2075             'lifetime' => 'referencelifetime');
2077         // id is specifically named to prevent overlaping between the two tables.
2078         $fields = array();
2079         $fields[] = $filesprefix.'.id AS id';
2080         foreach ($filefields as $field) {
2081             $fields[] = "{$filesprefix}.{$field}";
2082         }
2084         foreach ($referencefields as $field => $alias) {
2085             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2086         }
2088         return implode(', ', $fields);
2089     }
2091     /**
2092      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2093      *
2094      * If the record already exists, its id is returned. If there is no such record yet,
2095      * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
2096      *
2097      * @param int $repositoryid
2098      * @param string $reference
2099      * @return int
2100      */
2101     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2102         global $DB;
2104         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2106         if ($id !== false) {
2107             // bah, that was easy
2108             return $id;
2109         }
2111         // no such record yet, create one
2112         try {
2113             $id = $DB->insert_record('files_reference', array(
2114                 'repositoryid'  => $repositoryid,
2115                 'reference'     => $reference,
2116                 'referencehash' => sha1($reference),
2117                 'lastsync'      => $lastsync,
2118                 'lifetime'      => $lifetime));
2119         } catch (dml_exception $e) {
2120             // if inserting the new record failed, chances are that the race condition has just
2121             // occured and the unique index did not allow to create the second record with the same
2122             // repositoryid + reference combo
2123             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2124         }
2126         return $id;
2127     }
2129     /**
2130      * Returns the id of the record in {files_reference} that matches the passed parameters
2131      *
2132      * Depending on the required strictness, false can be returned. The behaviour is consistent
2133      * with standard DML methods.
2134      *
2135      * @param int $repositoryid
2136      * @param string $reference
2137      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2138      * @return int|bool
2139      */
2140     private function get_referencefileid($repositoryid, $reference, $strictness) {
2141         global $DB;
2143         return $DB->get_field('files_reference', 'id',
2144             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2145     }
2147     /**
2148      * Updates a reference to the external resource and all files that use it
2149      *
2150      * This function is called after synchronisation of an external file and updates the
2151      * contenthash, filesize and status of all files that reference this external file
2152      * as well as time last synchronised and sync lifetime (how long we don't need to call
2153      * synchronisation for this reference).
2154      *
2155      * @param int $referencefileid
2156      * @param int $lastsync
2157      * @param int $lifetime
2158      * @param string $contenthash
2159      * @param int $filesize
2160      * @param int $status 0 if ok or 666 if source is missing
2161      */
2162     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2163         global $DB;
2164         $referencefileid = clean_param($referencefileid, PARAM_INT);
2165         $lastsync = clean_param($lastsync, PARAM_INT);
2166         $lifetime = clean_param($lifetime, PARAM_INT);
2167         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2168         $filesize = clean_param($filesize, PARAM_INT);
2169         $status = clean_param($status, PARAM_INT);
2170         $params = array('contenthash' => $contenthash,
2171                     'filesize' => $filesize,
2172                     'status' => $status,
2173                     'referencefileid' => $referencefileid,
2174                     'lastsync' => $lastsync,
2175                     'lifetime' => $lifetime);
2176         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2177             status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
2178             WHERE referencefileid = :referencefileid', $params);
2179         $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2180         $DB->update_record('files_reference', (object)$data);
2181     }