MDL-38261 Add a new file preview mode for bigger thumbnails
[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 if ($mode === 'bigthumb') {
338             $data = generate_image_thumbnail($tmpfilepath, 250, 250);
340         } else {
341             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
342         }
344         unlink($tmpfilepath);
346         return $data;
347     }
349     /**
350      * Fetch file using local file id.
351      *
352      * Please do not rely on file ids, it is usually easier to use
353      * pathname hashes instead.
354      *
355      * @param int $fileid file ID
356      * @return stored_file|bool stored_file instance if exists, false if not
357      */
358     public function get_file_by_id($fileid) {
359         global $DB;
361         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
362                   FROM {files} f
363              LEFT JOIN {files_reference} r
364                        ON f.referencefileid = r.id
365                  WHERE f.id = ?";
366         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
367             return $this->get_file_instance($filerecord);
368         } else {
369             return false;
370         }
371     }
373     /**
374      * Fetch file using local file full pathname hash
375      *
376      * @param string $pathnamehash path name hash
377      * @return stored_file|bool stored_file instance if exists, false if not
378      */
379     public function get_file_by_hash($pathnamehash) {
380         global $DB;
382         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
383                   FROM {files} f
384              LEFT JOIN {files_reference} r
385                        ON f.referencefileid = r.id
386                  WHERE f.pathnamehash = ?";
387         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
388             return $this->get_file_instance($filerecord);
389         } else {
390             return false;
391         }
392     }
394     /**
395      * Fetch locally stored file.
396      *
397      * @param int $contextid context ID
398      * @param string $component component
399      * @param string $filearea file area
400      * @param int $itemid item ID
401      * @param string $filepath file path
402      * @param string $filename file name
403      * @return stored_file|bool stored_file instance if exists, false if not
404      */
405     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
406         $filepath = clean_param($filepath, PARAM_PATH);
407         $filename = clean_param($filename, PARAM_FILE);
409         if ($filename === '') {
410             $filename = '.';
411         }
413         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
414         return $this->get_file_by_hash($pathnamehash);
415     }
417     /**
418      * Are there any files (or directories)
419      *
420      * @param int $contextid context ID
421      * @param string $component component
422      * @param string $filearea file area
423      * @param bool|int $itemid item id or false if all items
424      * @param bool $ignoredirs whether or not ignore directories
425      * @return bool empty
426      */
427     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
428         global $DB;
430         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
431         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
433         if ($itemid !== false) {
434             $params['itemid'] = $itemid;
435             $where .= " AND itemid = :itemid";
436         }
438         if ($ignoredirs) {
439             $sql = "SELECT 'x'
440                       FROM {files}
441                      WHERE $where AND filename <> '.'";
442         } else {
443             $sql = "SELECT 'x'
444                       FROM {files}
445                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
446         }
448         return !$DB->record_exists_sql($sql, $params);
449     }
451     /**
452      * Returns all files belonging to given repository
453      *
454      * @param int $repositoryid
455      * @param string $sort A fragment of SQL to use for sorting
456      */
457     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
458         global $DB;
459         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
460                   FROM {files} f
461              LEFT JOIN {files_reference} r
462                        ON f.referencefileid = r.id
463                  WHERE r.repositoryid = ?";
464         if (!empty($sort)) {
465             $sql .= " ORDER BY {$sort}";
466         }
468         $result = array();
469         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
470         foreach ($filerecords as $filerecord) {
471             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
472         }
473         return $result;
474     }
476     /**
477      * Returns all area files (optionally limited by itemid)
478      *
479      * @param int $contextid context ID
480      * @param string $component component
481      * @param string $filearea file area
482      * @param int $itemid item ID or all files if not specified
483      * @param string $sort A fragment of SQL to use for sorting
484      * @param bool $includedirs whether or not include directories
485      * @return array of stored_files indexed by pathanmehash
486      */
487     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
488         global $DB;
490         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
491         if ($itemid !== false) {
492             $itemidsql = ' AND f.itemid = :itemid ';
493             $conditions['itemid'] = $itemid;
494         } else {
495             $itemidsql = '';
496         }
498         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
499                   FROM {files} f
500              LEFT JOIN {files_reference} r
501                        ON f.referencefileid = r.id
502                  WHERE f.contextid = :contextid
503                        AND f.component = :component
504                        AND f.filearea = :filearea
505                        $itemidsql";
506         if (!empty($sort)) {
507             $sql .= " ORDER BY {$sort}";
508         }
510         $result = array();
511         $filerecords = $DB->get_records_sql($sql, $conditions);
512         foreach ($filerecords as $filerecord) {
513             if (!$includedirs and $filerecord->filename === '.') {
514                 continue;
515             }
516             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
517         }
518         return $result;
519     }
521     /**
522      * Returns array based tree structure of area files
523      *
524      * @param int $contextid context ID
525      * @param string $component component
526      * @param string $filearea file area
527      * @param int $itemid item ID
528      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
529      */
530     public function get_area_tree($contextid, $component, $filearea, $itemid) {
531         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
532         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
533         // first create directory structure
534         foreach ($files as $hash=>$dir) {
535             if (!$dir->is_directory()) {
536                 continue;
537             }
538             unset($files[$hash]);
539             if ($dir->get_filepath() === '/') {
540                 $result['dirfile'] = $dir;
541                 continue;
542             }
543             $parts = explode('/', trim($dir->get_filepath(),'/'));
544             $pointer =& $result;
545             foreach ($parts as $part) {
546                 if ($part === '') {
547                     continue;
548                 }
549                 if (!isset($pointer['subdirs'][$part])) {
550                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
551                 }
552                 $pointer =& $pointer['subdirs'][$part];
553             }
554             $pointer['dirfile'] = $dir;
555             unset($pointer);
556         }
557         foreach ($files as $hash=>$file) {
558             $parts = explode('/', trim($file->get_filepath(),'/'));
559             $pointer =& $result;
560             foreach ($parts as $part) {
561                 if ($part === '') {
562                     continue;
563                 }
564                 $pointer =& $pointer['subdirs'][$part];
565             }
566             $pointer['files'][$file->get_filename()] = $file;
567             unset($pointer);
568         }
569         $result = $this->sort_area_tree($result);
570         return $result;
571     }
573     /**
574      * Sorts the result of {@link file_storage::get_area_tree()}.
575      *
576      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
577      * @return array of sorted results
578      */
579     protected function sort_area_tree($tree) {
580         foreach ($tree as $key => &$value) {
581             if ($key == 'subdirs') {
582                 $value = $this->sort_area_tree($value);
583                 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
584             } else if ($key == 'files') {
585                 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
586             }
587         }
588         return $tree;
589     }
591     /**
592      * Returns all files and optionally directories
593      *
594      * @param int $contextid context ID
595      * @param string $component component
596      * @param string $filearea file area
597      * @param int $itemid item ID
598      * @param int $filepath directory path
599      * @param bool $recursive include all subdirectories
600      * @param bool $includedirs include files and directories
601      * @param string $sort A fragment of SQL to use for sorting
602      * @return array of stored_files indexed by pathanmehash
603      */
604     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
605         global $DB;
607         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
608             return array();
609         }
611         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
613         if ($recursive) {
615             $dirs = $includedirs ? "" : "AND filename <> '.'";
616             $length = textlib::strlen($filepath);
618             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
619                       FROM {files} f
620                  LEFT JOIN {files_reference} r
621                            ON f.referencefileid = r.id
622                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
623                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
624                            AND f.id <> :dirid
625                            $dirs
626                            $orderby";
627             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
629             $files = array();
630             $dirs  = array();
631             $filerecords = $DB->get_records_sql($sql, $params);
632             foreach ($filerecords as $filerecord) {
633                 if ($filerecord->filename == '.') {
634                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
635                 } else {
636                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
637                 }
638             }
639             $result = array_merge($dirs, $files);
641         } else {
642             $result = array();
643             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
645             $length = textlib::strlen($filepath);
647             if ($includedirs) {
648                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
649                           FROM {files} f
650                      LEFT JOIN {files_reference} r
651                                ON f.referencefileid = r.id
652                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
653                                AND f.itemid = :itemid AND f.filename = '.'
654                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
655                                AND f.id <> :dirid
656                                $orderby";
657                 $reqlevel = substr_count($filepath, '/') + 1;
658                 $filerecords = $DB->get_records_sql($sql, $params);
659                 foreach ($filerecords as $filerecord) {
660                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
661                         continue;
662                     }
663                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
664                 }
665             }
667             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
668                       FROM {files} f
669                  LEFT JOIN {files_reference} r
670                            ON f.referencefileid = r.id
671                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
672                            AND f.filepath = :filepath AND f.filename <> '.'
673                            $orderby";
675             $filerecords = $DB->get_records_sql($sql, $params);
676             foreach ($filerecords as $filerecord) {
677                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
678             }
679         }
681         return $result;
682     }
684     /**
685      * Delete all area files (optionally limited by itemid).
686      *
687      * @param int $contextid context ID
688      * @param string $component component
689      * @param string $filearea file area or all areas in context if not specified
690      * @param int $itemid item ID or all files if not specified
691      * @return bool success
692      */
693     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
694         global $DB;
696         $conditions = array('contextid'=>$contextid);
697         if ($component !== false) {
698             $conditions['component'] = $component;
699         }
700         if ($filearea !== false) {
701             $conditions['filearea'] = $filearea;
702         }
703         if ($itemid !== false) {
704             $conditions['itemid'] = $itemid;
705         }
707         $filerecords = $DB->get_records('files', $conditions);
708         foreach ($filerecords as $filerecord) {
709             $this->get_file_instance($filerecord)->delete();
710         }
712         return true; // BC only
713     }
715     /**
716      * Delete all the files from certain areas where itemid is limited by an
717      * arbitrary bit of SQL.
718      *
719      * @param int $contextid the id of the context the files belong to. Must be given.
720      * @param string $component the owning component. Must be given.
721      * @param string $filearea the file area name. Must be given.
722      * @param string $itemidstest an SQL fragment that the itemid must match. Used
723      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
724      *      and may not used named parameters called contextid, component or filearea.
725      * @param array $params any query params used by $itemidstest.
726      */
727     public function delete_area_files_select($contextid, $component,
728             $filearea, $itemidstest, array $params = null) {
729         global $DB;
731         $where = "contextid = :contextid
732                 AND component = :component
733                 AND filearea = :filearea
734                 AND itemid $itemidstest";
735         $params['contextid'] = $contextid;
736         $params['component'] = $component;
737         $params['filearea'] = $filearea;
739         $filerecords = $DB->get_recordset_select('files', $where, $params);
740         foreach ($filerecords as $filerecord) {
741             $this->get_file_instance($filerecord)->delete();
742         }
743         $filerecords->close();
744     }
746     /**
747      * Move all the files in a file area from one context to another.
748      *
749      * @param int $oldcontextid the context the files are being moved from.
750      * @param int $newcontextid the context the files are being moved to.
751      * @param string $component the plugin that these files belong to.
752      * @param string $filearea the name of the file area.
753      * @param int $itemid file item ID
754      * @return int the number of files moved, for information.
755      */
756     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
757         // Note, this code is based on some code that Petr wrote in
758         // forum_move_attachments in mod/forum/lib.php. I moved it here because
759         // I needed it in the question code too.
760         $count = 0;
762         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
763         foreach ($oldfiles as $oldfile) {
764             $filerecord = new stdClass();
765             $filerecord->contextid = $newcontextid;
766             $this->create_file_from_storedfile($filerecord, $oldfile);
767             $count += 1;
768         }
770         if ($count) {
771             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
772         }
774         return $count;
775     }
777     /**
778      * Recursively creates directory.
779      *
780      * @param int $contextid context ID
781      * @param string $component component
782      * @param string $filearea file area
783      * @param int $itemid item ID
784      * @param string $filepath file path
785      * @param int $userid the user ID
786      * @return bool success
787      */
788     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
789         global $DB;
791         // validate all parameters, we do not want any rubbish stored in database, right?
792         if (!is_number($contextid) or $contextid < 1) {
793             throw new file_exception('storedfileproblem', 'Invalid contextid');
794         }
796         $component = clean_param($component, PARAM_COMPONENT);
797         if (empty($component)) {
798             throw new file_exception('storedfileproblem', 'Invalid component');
799         }
801         $filearea = clean_param($filearea, PARAM_AREA);
802         if (empty($filearea)) {
803             throw new file_exception('storedfileproblem', 'Invalid filearea');
804         }
806         if (!is_number($itemid) or $itemid < 0) {
807             throw new file_exception('storedfileproblem', 'Invalid itemid');
808         }
810         $filepath = clean_param($filepath, PARAM_PATH);
811         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
812             // path must start and end with '/'
813             throw new file_exception('storedfileproblem', 'Invalid file path');
814         }
816         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
818         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
819             return $dir_info;
820         }
822         static $contenthash = null;
823         if (!$contenthash) {
824             $this->add_string_to_pool('');
825             $contenthash = sha1('');
826         }
828         $now = time();
830         $dir_record = new stdClass();
831         $dir_record->contextid = $contextid;
832         $dir_record->component = $component;
833         $dir_record->filearea  = $filearea;
834         $dir_record->itemid    = $itemid;
835         $dir_record->filepath  = $filepath;
836         $dir_record->filename  = '.';
837         $dir_record->contenthash  = $contenthash;
838         $dir_record->filesize  = 0;
840         $dir_record->timecreated  = $now;
841         $dir_record->timemodified = $now;
842         $dir_record->mimetype     = null;
843         $dir_record->userid       = $userid;
845         $dir_record->pathnamehash = $pathnamehash;
847         $DB->insert_record('files', $dir_record);
848         $dir_info = $this->get_file_by_hash($pathnamehash);
850         if ($filepath !== '/') {
851             //recurse to parent dirs
852             $filepath = trim($filepath, '/');
853             $filepath = explode('/', $filepath);
854             array_pop($filepath);
855             $filepath = implode('/', $filepath);
856             $filepath = ($filepath === '') ? '/' : "/$filepath/";
857             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
858         }
860         return $dir_info;
861     }
863     /**
864      * Add new local file based on existing local file.
865      *
866      * @param stdClass|array $filerecord object or array describing changes
867      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
868      * @return stored_file instance of newly created file
869      */
870     public function create_file_from_storedfile($filerecord, $fileorid) {
871         global $DB;
873         if ($fileorid instanceof stored_file) {
874             $fid = $fileorid->get_id();
875         } else {
876             $fid = $fileorid;
877         }
879         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
881         unset($filerecord['id']);
882         unset($filerecord['filesize']);
883         unset($filerecord['contenthash']);
884         unset($filerecord['pathnamehash']);
886         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
887                   FROM {files} f
888              LEFT JOIN {files_reference} r
889                        ON f.referencefileid = r.id
890                  WHERE f.id = ?";
892         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
893             throw new file_exception('storedfileproblem', 'File does not exist');
894         }
896         unset($newrecord->id);
898         foreach ($filerecord as $key => $value) {
899             // validate all parameters, we do not want any rubbish stored in database, right?
900             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
901                 throw new file_exception('storedfileproblem', 'Invalid contextid');
902             }
904             if ($key == 'component') {
905                 $value = clean_param($value, PARAM_COMPONENT);
906                 if (empty($value)) {
907                     throw new file_exception('storedfileproblem', 'Invalid component');
908                 }
909             }
911             if ($key == 'filearea') {
912                 $value = clean_param($value, PARAM_AREA);
913                 if (empty($value)) {
914                     throw new file_exception('storedfileproblem', 'Invalid filearea');
915                 }
916             }
918             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
919                 throw new file_exception('storedfileproblem', 'Invalid itemid');
920             }
923             if ($key == 'filepath') {
924                 $value = clean_param($value, PARAM_PATH);
925                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
926                     // path must start and end with '/'
927                     throw new file_exception('storedfileproblem', 'Invalid file path');
928                 }
929             }
931             if ($key == 'filename') {
932                 $value = clean_param($value, PARAM_FILE);
933                 if ($value === '') {
934                     // path must start and end with '/'
935                     throw new file_exception('storedfileproblem', 'Invalid file name');
936                 }
937             }
939             if ($key === 'timecreated' or $key === 'timemodified') {
940                 if (!is_number($value)) {
941                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
942                 }
943                 if ($value < 0) {
944                     //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)
945                     $value = 0;
946                 }
947             }
949             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
950                 $value = clean_param($value, PARAM_INT);
951             }
953             $newrecord->$key = $value;
954         }
956         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
958         if ($newrecord->filename === '.') {
959             // special case - only this function supports directories ;-)
960             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
961             // update the existing directory with the new data
962             $newrecord->id = $directory->get_id();
963             $DB->update_record('files', $newrecord);
964             return $this->get_file_instance($newrecord);
965         }
967         // note: referencefileid is copied from the original file so that
968         // creating a new file from an existing alias creates new alias implicitly.
969         // here we just check the database consistency.
970         if (!empty($newrecord->repositoryid)) {
971             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
972                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
973             }
974         }
976         try {
977             $newrecord->id = $DB->insert_record('files', $newrecord);
978         } catch (dml_exception $e) {
979             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
980                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
981         }
984         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
986         return $this->get_file_instance($newrecord);
987     }
989     /**
990      * Add new local file.
991      *
992      * @param stdClass|array $filerecord object or array describing file
993      * @param string $url the URL to the file
994      * @param array $options {@link download_file_content()} options
995      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
996      * @return stored_file
997      */
998     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1000         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1001         $filerecord = (object)$filerecord; // We support arrays too.
1003         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1004         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1005         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1006         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1007         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1008         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1009         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1011         if (!isset($filerecord->filename)) {
1012             $parts = explode('/', $url);
1013             $filename = array_pop($parts);
1014             $filerecord->filename = clean_param($filename, PARAM_FILE);
1015         }
1016         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1017         $filerecord->source = clean_param($source, PARAM_URL);
1019         if ($usetempfile) {
1020             check_dir_exists($this->tempdir);
1021             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1022             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1023             if ($content === false) {
1024                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1025             }
1026             try {
1027                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1028                 @unlink($tmpfile);
1029                 return $newfile;
1030             } catch (Exception $e) {
1031                 @unlink($tmpfile);
1032                 throw $e;
1033             }
1035         } else {
1036             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1037             if ($content === false) {
1038                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1039             }
1040             return $this->create_file_from_string($filerecord, $content);
1041         }
1042     }
1044     /**
1045      * Add new local file.
1046      *
1047      * @param stdClass|array $filerecord object or array describing file
1048      * @param string $pathname path to file or content of file
1049      * @return stored_file
1050      */
1051     public function create_file_from_pathname($filerecord, $pathname) {
1052         global $DB;
1054         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1055         $filerecord = (object)$filerecord; // We support arrays too.
1057         // validate all parameters, we do not want any rubbish stored in database, right?
1058         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1059             throw new file_exception('storedfileproblem', 'Invalid contextid');
1060         }
1062         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1063         if (empty($filerecord->component)) {
1064             throw new file_exception('storedfileproblem', 'Invalid component');
1065         }
1067         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1068         if (empty($filerecord->filearea)) {
1069             throw new file_exception('storedfileproblem', 'Invalid filearea');
1070         }
1072         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1073             throw new file_exception('storedfileproblem', 'Invalid itemid');
1074         }
1076         if (!empty($filerecord->sortorder)) {
1077             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1078                 $filerecord->sortorder = 0;
1079             }
1080         } else {
1081             $filerecord->sortorder = 0;
1082         }
1084         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1085         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1086             // path must start and end with '/'
1087             throw new file_exception('storedfileproblem', 'Invalid file path');
1088         }
1090         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1091         if ($filerecord->filename === '') {
1092             // filename must not be empty
1093             throw new file_exception('storedfileproblem', 'Invalid file name');
1094         }
1096         $now = time();
1097         if (isset($filerecord->timecreated)) {
1098             if (!is_number($filerecord->timecreated)) {
1099                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1100             }
1101             if ($filerecord->timecreated < 0) {
1102                 //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)
1103                 $filerecord->timecreated = 0;
1104             }
1105         } else {
1106             $filerecord->timecreated = $now;
1107         }
1109         if (isset($filerecord->timemodified)) {
1110             if (!is_number($filerecord->timemodified)) {
1111                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1112             }
1113             if ($filerecord->timemodified < 0) {
1114                 //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)
1115                 $filerecord->timemodified = 0;
1116             }
1117         } else {
1118             $filerecord->timemodified = $now;
1119         }
1121         $newrecord = new stdClass();
1123         $newrecord->contextid = $filerecord->contextid;
1124         $newrecord->component = $filerecord->component;
1125         $newrecord->filearea  = $filerecord->filearea;
1126         $newrecord->itemid    = $filerecord->itemid;
1127         $newrecord->filepath  = $filerecord->filepath;
1128         $newrecord->filename  = $filerecord->filename;
1130         $newrecord->timecreated  = $filerecord->timecreated;
1131         $newrecord->timemodified = $filerecord->timemodified;
1132         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1133         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1134         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1135         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1136         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1137         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1138         $newrecord->sortorder    = $filerecord->sortorder;
1140         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1142         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1144         try {
1145             $newrecord->id = $DB->insert_record('files', $newrecord);
1146         } catch (dml_exception $e) {
1147             if ($newfile) {
1148                 $this->deleted_file_cleanup($newrecord->contenthash);
1149             }
1150             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1151                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1152         }
1154         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1156         return $this->get_file_instance($newrecord);
1157     }
1159     /**
1160      * Add new local file.
1161      *
1162      * @param stdClass|array $filerecord object or array describing file
1163      * @param string $content content of file
1164      * @return stored_file
1165      */
1166     public function create_file_from_string($filerecord, $content) {
1167         global $DB;
1169         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1170         $filerecord = (object)$filerecord; // We support arrays too.
1172         // validate all parameters, we do not want any rubbish stored in database, right?
1173         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1174             throw new file_exception('storedfileproblem', 'Invalid contextid');
1175         }
1177         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1178         if (empty($filerecord->component)) {
1179             throw new file_exception('storedfileproblem', 'Invalid component');
1180         }
1182         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1183         if (empty($filerecord->filearea)) {
1184             throw new file_exception('storedfileproblem', 'Invalid filearea');
1185         }
1187         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1188             throw new file_exception('storedfileproblem', 'Invalid itemid');
1189         }
1191         if (!empty($filerecord->sortorder)) {
1192             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1193                 $filerecord->sortorder = 0;
1194             }
1195         } else {
1196             $filerecord->sortorder = 0;
1197         }
1199         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1200         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1201             // path must start and end with '/'
1202             throw new file_exception('storedfileproblem', 'Invalid file path');
1203         }
1205         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1206         if ($filerecord->filename === '') {
1207             // path must start and end with '/'
1208             throw new file_exception('storedfileproblem', 'Invalid file name');
1209         }
1211         $now = time();
1212         if (isset($filerecord->timecreated)) {
1213             if (!is_number($filerecord->timecreated)) {
1214                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1215             }
1216             if ($filerecord->timecreated < 0) {
1217                 //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)
1218                 $filerecord->timecreated = 0;
1219             }
1220         } else {
1221             $filerecord->timecreated = $now;
1222         }
1224         if (isset($filerecord->timemodified)) {
1225             if (!is_number($filerecord->timemodified)) {
1226                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1227             }
1228             if ($filerecord->timemodified < 0) {
1229                 //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)
1230                 $filerecord->timemodified = 0;
1231             }
1232         } else {
1233             $filerecord->timemodified = $now;
1234         }
1236         $newrecord = new stdClass();
1238         $newrecord->contextid = $filerecord->contextid;
1239         $newrecord->component = $filerecord->component;
1240         $newrecord->filearea  = $filerecord->filearea;
1241         $newrecord->itemid    = $filerecord->itemid;
1242         $newrecord->filepath  = $filerecord->filepath;
1243         $newrecord->filename  = $filerecord->filename;
1245         $newrecord->timecreated  = $filerecord->timecreated;
1246         $newrecord->timemodified = $filerecord->timemodified;
1247         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1248         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1249         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1250         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1251         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1252         $newrecord->sortorder    = $filerecord->sortorder;
1254         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1255         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1256         // get mimetype by magic bytes
1257         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1259         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1261         try {
1262             $newrecord->id = $DB->insert_record('files', $newrecord);
1263         } catch (dml_exception $e) {
1264             if ($newfile) {
1265                 $this->deleted_file_cleanup($newrecord->contenthash);
1266             }
1267             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1268                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1269         }
1271         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1273         return $this->get_file_instance($newrecord);
1274     }
1276     /**
1277      * Create a new alias/shortcut file from file reference information
1278      *
1279      * @param stdClass|array $filerecord object or array describing the new file
1280      * @param int $repositoryid the id of the repository that provides the original file
1281      * @param string $reference the information required by the repository to locate the original file
1282      * @param array $options options for creating the new file
1283      * @return stored_file
1284      */
1285     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1286         global $DB;
1288         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1289         $filerecord = (object)$filerecord; // We support arrays too.
1291         // validate all parameters, we do not want any rubbish stored in database, right?
1292         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1293             throw new file_exception('storedfileproblem', 'Invalid contextid');
1294         }
1296         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1297         if (empty($filerecord->component)) {
1298             throw new file_exception('storedfileproblem', 'Invalid component');
1299         }
1301         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1302         if (empty($filerecord->filearea)) {
1303             throw new file_exception('storedfileproblem', 'Invalid filearea');
1304         }
1306         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1307             throw new file_exception('storedfileproblem', 'Invalid itemid');
1308         }
1310         if (!empty($filerecord->sortorder)) {
1311             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1312                 $filerecord->sortorder = 0;
1313             }
1314         } else {
1315             $filerecord->sortorder = 0;
1316         }
1318         // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
1319         unset($filerecord->referencelastsync);
1320         unset($filerecord->referencelifetime);
1322         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1323         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1324         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1325         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1326         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1327         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1328         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1329         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1330             // Path must start and end with '/'.
1331             throw new file_exception('storedfileproblem', 'Invalid file path');
1332         }
1334         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1335         if ($filerecord->filename === '') {
1336             // Path must start and end with '/'.
1337             throw new file_exception('storedfileproblem', 'Invalid file name');
1338         }
1340         $now = time();
1341         if (isset($filerecord->timecreated)) {
1342             if (!is_number($filerecord->timecreated)) {
1343                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1344             }
1345             if ($filerecord->timecreated < 0) {
1346                 // 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)
1347                 $filerecord->timecreated = 0;
1348             }
1349         } else {
1350             $filerecord->timecreated = $now;
1351         }
1353         if (isset($filerecord->timemodified)) {
1354             if (!is_number($filerecord->timemodified)) {
1355                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1356             }
1357             if ($filerecord->timemodified < 0) {
1358                 // 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)
1359                 $filerecord->timemodified = 0;
1360             }
1361         } else {
1362             $filerecord->timemodified = $now;
1363         }
1365         $transaction = $DB->start_delegated_transaction();
1367         try {
1368             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1369         } catch (Exception $e) {
1370             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1371         }
1373         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1374             // there was specified the contenthash for a file already stored in moodle filepool
1375             if (empty($filerecord->filesize)) {
1376                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1377                 $filerecord->filesize = filesize($filepathname);
1378             } else {
1379                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1380             }
1381         } else {
1382             // atempt to get the result of last synchronisation for this reference
1383             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1384                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1385             if ($lastcontent) {
1386                 $filerecord->contenthash = $lastcontent->contenthash;
1387                 $filerecord->filesize = $lastcontent->filesize;
1388             } else {
1389                 // External file doesn't have content in moodle.
1390                 // So we create an empty file for it.
1391                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1392             }
1393         }
1395         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1397         try {
1398             $filerecord->id = $DB->insert_record('files', $filerecord);
1399         } catch (dml_exception $e) {
1400             if (!empty($newfile)) {
1401                 $this->deleted_file_cleanup($filerecord->contenthash);
1402             }
1403             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1404                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1405         }
1407         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1409         $transaction->allow_commit();
1411         // this will retrieve all reference information from DB as well
1412         return $this->get_file_by_id($filerecord->id);
1413     }
1415     /**
1416      * Creates new image file from existing.
1417      *
1418      * @param stdClass|array $filerecord object or array describing new file
1419      * @param int|stored_file $fid file id or stored file object
1420      * @param int $newwidth in pixels
1421      * @param int $newheight in pixels
1422      * @param bool $keepaspectratio whether or not keep aspect ratio
1423      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1424      * @return stored_file
1425      */
1426     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1427         if (!function_exists('imagecreatefromstring')) {
1428             //Most likely the GD php extension isn't installed
1429             //image conversion cannot succeed
1430             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1431         }
1433         if ($fid instanceof stored_file) {
1434             $fid = $fid->get_id();
1435         }
1437         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1439         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1440             throw new file_exception('storedfileproblem', 'File does not exist');
1441         }
1443         if (!$imageinfo = $file->get_imageinfo()) {
1444             throw new file_exception('storedfileproblem', 'File is not an image');
1445         }
1447         if (!isset($filerecord['filename'])) {
1448             $filerecord['filename'] = $file->get_filename();
1449         }
1451         if (!isset($filerecord['mimetype'])) {
1452             $filerecord['mimetype'] = $imageinfo['mimetype'];
1453         }
1455         $width    = $imageinfo['width'];
1456         $height   = $imageinfo['height'];
1457         $mimetype = $imageinfo['mimetype'];
1459         if ($keepaspectratio) {
1460             if (0 >= $newwidth and 0 >= $newheight) {
1461                 // no sizes specified
1462                 $newwidth  = $width;
1463                 $newheight = $height;
1465             } else if (0 < $newwidth and 0 < $newheight) {
1466                 $xheight = ($newwidth*($height/$width));
1467                 if ($xheight < $newheight) {
1468                     $newheight = (int)$xheight;
1469                 } else {
1470                     $newwidth = (int)($newheight*($width/$height));
1471                 }
1473             } else if (0 < $newwidth) {
1474                 $newheight = (int)($newwidth*($height/$width));
1476             } else { //0 < $newheight
1477                 $newwidth = (int)($newheight*($width/$height));
1478             }
1480         } else {
1481             if (0 >= $newwidth) {
1482                 $newwidth = $width;
1483             }
1484             if (0 >= $newheight) {
1485                 $newheight = $height;
1486             }
1487         }
1489         $img = imagecreatefromstring($file->get_content());
1490         if ($height != $newheight or $width != $newwidth) {
1491             $newimg = imagecreatetruecolor($newwidth, $newheight);
1492             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1493                 // weird
1494                 throw new file_exception('storedfileproblem', 'Can not resize image');
1495             }
1496             imagedestroy($img);
1497             $img = $newimg;
1498         }
1500         ob_start();
1501         switch ($filerecord['mimetype']) {
1502             case 'image/gif':
1503                 imagegif($img);
1504                 break;
1506             case 'image/jpeg':
1507                 if (is_null($quality)) {
1508                     imagejpeg($img);
1509                 } else {
1510                     imagejpeg($img, NULL, $quality);
1511                 }
1512                 break;
1514             case 'image/png':
1515                 $quality = (int)$quality;
1516                 imagepng($img, NULL, $quality, NULL);
1517                 break;
1519             default:
1520                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1521         }
1523         $content = ob_get_contents();
1524         ob_end_clean();
1525         imagedestroy($img);
1527         if (!$content) {
1528             throw new file_exception('storedfileproblem', 'Can not convert image');
1529         }
1531         return $this->create_file_from_string($filerecord, $content);
1532     }
1534     /**
1535      * Add file content to sha1 pool.
1536      *
1537      * @param string $pathname path to file
1538      * @param string $contenthash sha1 hash of content if known (performance only)
1539      * @return array (contenthash, filesize, newfile)
1540      */
1541     public function add_file_to_pool($pathname, $contenthash = NULL) {
1542         if (!is_readable($pathname)) {
1543             throw new file_exception('storedfilecannotread', '', $pathname);
1544         }
1546         if (is_null($contenthash)) {
1547             $contenthash = sha1_file($pathname);
1548         }
1550         $filesize = filesize($pathname);
1552         $hashpath = $this->path_from_hash($contenthash);
1553         $hashfile = "$hashpath/$contenthash";
1555         if (file_exists($hashfile)) {
1556             if (filesize($hashfile) !== $filesize) {
1557                 throw new file_pool_content_exception($contenthash);
1558             }
1559             $newfile = false;
1561         } else {
1562             if (!is_dir($hashpath)) {
1563                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1564                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1565                 }
1566             }
1567             $newfile = true;
1569             if (!copy($pathname, $hashfile)) {
1570                 throw new file_exception('storedfilecannotread', '', $pathname);
1571             }
1573             if (filesize($hashfile) !== $filesize) {
1574                 @unlink($hashfile);
1575                 throw new file_pool_content_exception($contenthash);
1576             }
1577             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1578         }
1581         return array($contenthash, $filesize, $newfile);
1582     }
1584     /**
1585      * Add string content to sha1 pool.
1586      *
1587      * @param string $content file content - binary string
1588      * @return array (contenthash, filesize, newfile)
1589      */
1590     public function add_string_to_pool($content) {
1591         $contenthash = sha1($content);
1592         $filesize = strlen($content); // binary length
1594         $hashpath = $this->path_from_hash($contenthash);
1595         $hashfile = "$hashpath/$contenthash";
1598         if (file_exists($hashfile)) {
1599             if (filesize($hashfile) !== $filesize) {
1600                 throw new file_pool_content_exception($contenthash);
1601             }
1602             $newfile = false;
1604         } else {
1605             if (!is_dir($hashpath)) {
1606                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1607                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1608                 }
1609             }
1610             $newfile = true;
1612             file_put_contents($hashfile, $content);
1614             if (filesize($hashfile) !== $filesize) {
1615                 @unlink($hashfile);
1616                 throw new file_pool_content_exception($contenthash);
1617             }
1618             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1619         }
1621         return array($contenthash, $filesize, $newfile);
1622     }
1624     /**
1625      * Serve file content using X-Sendfile header.
1626      * Please make sure that all headers are already sent
1627      * and the all access control checks passed.
1628      *
1629      * @param string $contenthash sah1 hash of the file content to be served
1630      * @return bool success
1631      */
1632     public function xsendfile($contenthash) {
1633         global $CFG;
1634         require_once("$CFG->libdir/xsendfilelib.php");
1636         $hashpath = $this->path_from_hash($contenthash);
1637         return xsendfile("$hashpath/$contenthash");
1638     }
1640     /**
1641      * Content exists
1642      *
1643      * @param string $contenthash
1644      * @return bool
1645      */
1646     public function content_exists($contenthash) {
1647         $dir = $this->path_from_hash($contenthash);
1648         $filepath = $dir . '/' . $contenthash;
1649         return file_exists($filepath);
1650     }
1652     /**
1653      * Return path to file with given hash.
1654      *
1655      * NOTE: must not be public, files in pool must not be modified
1656      *
1657      * @param string $contenthash content hash
1658      * @return string expected file location
1659      */
1660     protected function path_from_hash($contenthash) {
1661         $l1 = $contenthash[0].$contenthash[1];
1662         $l2 = $contenthash[2].$contenthash[3];
1663         return "$this->filedir/$l1/$l2";
1664     }
1666     /**
1667      * Return path to file with given hash.
1668      *
1669      * NOTE: must not be public, files in pool must not be modified
1670      *
1671      * @param string $contenthash content hash
1672      * @return string expected file location
1673      */
1674     protected function trash_path_from_hash($contenthash) {
1675         $l1 = $contenthash[0].$contenthash[1];
1676         $l2 = $contenthash[2].$contenthash[3];
1677         return "$this->trashdir/$l1/$l2";
1678     }
1680     /**
1681      * Tries to recover missing content of file from trash.
1682      *
1683      * @param stored_file $file stored_file instance
1684      * @return bool success
1685      */
1686     public function try_content_recovery($file) {
1687         $contenthash = $file->get_contenthash();
1688         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1689         if (!is_readable($trashfile)) {
1690             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1691                 return false;
1692             }
1693             // nice, at least alternative trash file in trash root exists
1694             $trashfile = $this->trashdir.'/'.$contenthash;
1695         }
1696         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1697             //weird, better fail early
1698             return false;
1699         }
1700         $contentdir  = $this->path_from_hash($contenthash);
1701         $contentfile = $contentdir.'/'.$contenthash;
1702         if (file_exists($contentfile)) {
1703             //strange, no need to recover anything
1704             return true;
1705         }
1706         if (!is_dir($contentdir)) {
1707             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1708                 return false;
1709             }
1710         }
1711         return rename($trashfile, $contentfile);
1712     }
1714     /**
1715      * Marks pool file as candidate for deleting.
1716      *
1717      * DO NOT call directly - reserved for core!!
1718      *
1719      * @param string $contenthash
1720      */
1721     public function deleted_file_cleanup($contenthash) {
1722         global $DB;
1724         if ($contenthash === sha1('')) {
1725             // No need to delete empty content file with sha1('') content hash.
1726             return;
1727         }
1729         //Note: this section is critical - in theory file could be reused at the same
1730         //      time, if this happens we can still recover the file from trash
1731         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1732             // file content is still used
1733             return;
1734         }
1735         //move content file to trash
1736         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1737         if (!file_exists($contentfile)) {
1738             //weird, but no problem
1739             return;
1740         }
1741         $trashpath = $this->trash_path_from_hash($contenthash);
1742         $trashfile = $trashpath.'/'.$contenthash;
1743         if (file_exists($trashfile)) {
1744             // we already have this content in trash, no need to move it there
1745             unlink($contentfile);
1746             return;
1747         }
1748         if (!is_dir($trashpath)) {
1749             mkdir($trashpath, $this->dirpermissions, true);
1750         }
1751         rename($contentfile, $trashfile);
1752         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1753     }
1755     /**
1756      * When user referring to a moodle file, we build the reference field
1757      *
1758      * @param array $params
1759      * @return string
1760      */
1761     public static function pack_reference($params) {
1762         $params = (array)$params;
1763         $reference = array();
1764         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1765         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1766         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1767         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1768         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
1769         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1770         return base64_encode(serialize($reference));
1771     }
1773     /**
1774      * Unpack reference field
1775      *
1776      * @param string $str
1777      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
1778      * @throws file_reference_exception if the $str does not have the expected format
1779      * @return array
1780      */
1781     public static function unpack_reference($str, $cleanparams = false) {
1782         $decoded = base64_decode($str, true);
1783         if ($decoded === false) {
1784             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1785         }
1786         $params = @unserialize($decoded); // hide E_NOTICE
1787         if ($params === false) {
1788             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1789         }
1790         if (is_array($params) && $cleanparams) {
1791             $params = array(
1792                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
1793                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
1794                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
1795                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
1796                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
1797                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1798             );
1799         }
1800         return $params;
1801     }
1803     /**
1804      * Returns all aliases that refer to some stored_file via the given reference
1805      *
1806      * All repositories that provide access to a stored_file are expected to use
1807      * {@link self::pack_reference()}. This method can't be used if the given reference
1808      * does not use this format or if you are looking for references to an external file
1809      * (for example it can't be used to search for all aliases that refer to a given
1810      * Dropbox or Box.net file).
1811      *
1812      * Aliases in user draft areas are excluded from the returned list.
1813      *
1814      * @param string $reference identification of the referenced file
1815      * @return array of stored_file indexed by its pathnamehash
1816      */
1817     public function search_references($reference) {
1818         global $DB;
1820         if (is_null($reference)) {
1821             throw new coding_exception('NULL is not a valid reference to an external file');
1822         }
1824         // Give {@link self::unpack_reference()} a chance to throw exception if the
1825         // reference is not in a valid format.
1826         self::unpack_reference($reference);
1828         $referencehash = sha1($reference);
1830         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1831                   FROM {files} f
1832                   JOIN {files_reference} r ON f.referencefileid = r.id
1833                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1834                  WHERE r.referencehash = ?
1835                        AND (f.component <> ? OR f.filearea <> ?)";
1837         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
1838         $files = array();
1839         foreach ($rs as $filerecord) {
1840             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1841         }
1843         return $files;
1844     }
1846     /**
1847      * Returns the number of aliases that refer to some stored_file via the given reference
1848      *
1849      * All repositories that provide access to a stored_file are expected to use
1850      * {@link self::pack_reference()}. This method can't be used if the given reference
1851      * does not use this format or if you are looking for references to an external file
1852      * (for example it can't be used to count aliases that refer to a given Dropbox or
1853      * Box.net file).
1854      *
1855      * Aliases in user draft areas are not counted.
1856      *
1857      * @param string $reference identification of the referenced file
1858      * @return int
1859      */
1860     public function search_references_count($reference) {
1861         global $DB;
1863         if (is_null($reference)) {
1864             throw new coding_exception('NULL is not a valid reference to an external file');
1865         }
1867         // Give {@link self::unpack_reference()} a chance to throw exception if the
1868         // reference is not in a valid format.
1869         self::unpack_reference($reference);
1871         $referencehash = sha1($reference);
1873         $sql = "SELECT COUNT(f.id)
1874                   FROM {files} f
1875                   JOIN {files_reference} r ON f.referencefileid = r.id
1876                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
1877                  WHERE r.referencehash = ?
1878                        AND (f.component <> ? OR f.filearea <> ?)";
1880         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
1881     }
1883     /**
1884      * Returns all aliases that link to the given stored_file
1885      *
1886      * Aliases in user draft areas are excluded from the returned list.
1887      *
1888      * @param stored_file $storedfile
1889      * @return array of stored_file
1890      */
1891     public function get_references_by_storedfile(stored_file $storedfile) {
1892         global $DB;
1894         $params = array();
1895         $params['contextid'] = $storedfile->get_contextid();
1896         $params['component'] = $storedfile->get_component();
1897         $params['filearea']  = $storedfile->get_filearea();
1898         $params['itemid']    = $storedfile->get_itemid();
1899         $params['filename']  = $storedfile->get_filename();
1900         $params['filepath']  = $storedfile->get_filepath();
1902         return $this->search_references(self::pack_reference($params));
1903     }
1905     /**
1906      * Returns the number of aliases that link to the given stored_file
1907      *
1908      * Aliases in user draft areas are not counted.
1909      *
1910      * @param stored_file $storedfile
1911      * @return int
1912      */
1913     public function get_references_count_by_storedfile(stored_file $storedfile) {
1914         global $DB;
1916         $params = array();
1917         $params['contextid'] = $storedfile->get_contextid();
1918         $params['component'] = $storedfile->get_component();
1919         $params['filearea']  = $storedfile->get_filearea();
1920         $params['itemid']    = $storedfile->get_itemid();
1921         $params['filename']  = $storedfile->get_filename();
1922         $params['filepath']  = $storedfile->get_filepath();
1924         return $this->search_references_count(self::pack_reference($params));
1925     }
1927     /**
1928      * Updates all files that are referencing this file with the new contenthash
1929      * and filesize
1930      *
1931      * @param stored_file $storedfile
1932      */
1933     public function update_references_to_storedfile(stored_file $storedfile) {
1934         global $CFG, $DB;
1935         $params = array();
1936         $params['contextid'] = $storedfile->get_contextid();
1937         $params['component'] = $storedfile->get_component();
1938         $params['filearea']  = $storedfile->get_filearea();
1939         $params['itemid']    = $storedfile->get_itemid();
1940         $params['filename']  = $storedfile->get_filename();
1941         $params['filepath']  = $storedfile->get_filepath();
1942         $reference = self::pack_reference($params);
1943         $referencehash = sha1($reference);
1945         $sql = "SELECT repositoryid, id FROM {files_reference}
1946                  WHERE referencehash = ?";
1947         $rs = $DB->get_recordset_sql($sql, array($referencehash));
1949         $now = time();
1950         foreach ($rs as $record) {
1951             require_once($CFG->dirroot.'/repository/lib.php');
1952             $repo = repository::get_instance($record->repositoryid);
1953             $lifetime = $repo->get_reference_file_lifetime($reference);
1954             $this->update_references($record->id, $now, $lifetime,
1955                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
1956         }
1957         $rs->close();
1958     }
1960     /**
1961      * Convert file alias to local file
1962      *
1963      * @throws moodle_exception if file could not be downloaded
1964      *
1965      * @param stored_file $storedfile a stored_file instances
1966      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
1967      * @return stored_file stored_file
1968      */
1969     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
1970         global $CFG;
1971         $storedfile->import_external_file_contents($maxbytes);
1972         $storedfile->delete_reference();
1973         return $storedfile;
1974     }
1976     /**
1977      * Return mimetype by given file pathname
1978      *
1979      * If file has a known extension, we return the mimetype based on extension.
1980      * Otherwise (when possible) we try to get the mimetype from file contents.
1981      *
1982      * @param string $pathname full path to the file
1983      * @param string $filename correct file name with extension, if omitted will be taken from $path
1984      * @return string
1985      */
1986     public static function mimetype($pathname, $filename = null) {
1987         if (empty($filename)) {
1988             $filename = $pathname;
1989         }
1990         $type = mimeinfo('type', $filename);
1991         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1992             $finfo = new finfo(FILEINFO_MIME_TYPE);
1993             $type = mimeinfo_from_type('type', $finfo->file($pathname));
1994         }
1995         return $type;
1996     }
1998     /**
1999      * Cron cleanup job.
2000      */
2001     public function cron() {
2002         global $CFG, $DB;
2004         // find out all stale draft areas (older than 4 days) and purge them
2005         // those are identified by time stamp of the /. root dir
2006         mtrace('Deleting old draft files... ', '');
2007         $old = time() - 60*60*24*4;
2008         $sql = "SELECT *
2009                   FROM {files}
2010                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2011                        AND timecreated < :old";
2012         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2013         foreach ($rs as $dir) {
2014             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2015         }
2016         $rs->close();
2017         mtrace('done.');
2019         // remove orphaned preview files (that is files in the core preview filearea without
2020         // the existing original file)
2021         mtrace('Deleting orphaned preview files... ', '');
2022         $sql = "SELECT p.*
2023                   FROM {files} p
2024              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2025                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2026                        AND o.id IS NULL";
2027         $syscontext = context_system::instance();
2028         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2029         foreach ($rs as $orphan) {
2030             $file = $this->get_file_instance($orphan);
2031             if (!$file->is_directory()) {
2032                 $file->delete();
2033             }
2034         }
2035         $rs->close();
2036         mtrace('done.');
2038         // remove trash pool files once a day
2039         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2040         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2041             require_once($CFG->libdir.'/filelib.php');
2042             // Delete files that are associated with a context that no longer exists.
2043             mtrace('Cleaning up files from deleted contexts... ', '');
2044             $sql = "SELECT DISTINCT f.contextid
2045                     FROM {files} f
2046                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2047                     WHERE c.id IS NULL";
2048             $rs = $DB->get_recordset_sql($sql);
2049             if ($rs->valid()) {
2050                 $fs = get_file_storage();
2051                 foreach ($rs as $ctx) {
2052                     $fs->delete_area_files($ctx->contextid);
2053                 }
2054             }
2055             $rs->close();
2056             mtrace('done.');
2058             mtrace('Deleting trash files... ', '');
2059             fulldelete($this->trashdir);
2060             set_config('fileslastcleanup', time());
2061             mtrace('done.');
2062         }
2063     }
2065     /**
2066      * Get the sql formated fields for a file instance to be created from a
2067      * {files} and {files_refernece} join.
2068      *
2069      * @param string $filesprefix the table prefix for the {files} table
2070      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2071      * @return string the sql to go after a SELECT
2072      */
2073     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2074         // Note, these fieldnames MUST NOT overlap between the two tables,
2075         // else problems like MDL-33172 occur.
2076         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2077             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2078             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2080         $referencefields = array('repositoryid' => 'repositoryid',
2081             'reference' => 'reference',
2082             'lastsync' => 'referencelastsync',
2083             'lifetime' => 'referencelifetime');
2085         // id is specifically named to prevent overlaping between the two tables.
2086         $fields = array();
2087         $fields[] = $filesprefix.'.id AS id';
2088         foreach ($filefields as $field) {
2089             $fields[] = "{$filesprefix}.{$field}";
2090         }
2092         foreach ($referencefields as $field => $alias) {
2093             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2094         }
2096         return implode(', ', $fields);
2097     }
2099     /**
2100      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2101      *
2102      * If the record already exists, its id is returned. If there is no such record yet,
2103      * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
2104      *
2105      * @param int $repositoryid
2106      * @param string $reference
2107      * @return int
2108      */
2109     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2110         global $DB;
2112         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2114         if ($id !== false) {
2115             // bah, that was easy
2116             return $id;
2117         }
2119         // no such record yet, create one
2120         try {
2121             $id = $DB->insert_record('files_reference', array(
2122                 'repositoryid'  => $repositoryid,
2123                 'reference'     => $reference,
2124                 'referencehash' => sha1($reference),
2125                 'lastsync'      => $lastsync,
2126                 'lifetime'      => $lifetime));
2127         } catch (dml_exception $e) {
2128             // if inserting the new record failed, chances are that the race condition has just
2129             // occured and the unique index did not allow to create the second record with the same
2130             // repositoryid + reference combo
2131             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2132         }
2134         return $id;
2135     }
2137     /**
2138      * Returns the id of the record in {files_reference} that matches the passed parameters
2139      *
2140      * Depending on the required strictness, false can be returned. The behaviour is consistent
2141      * with standard DML methods.
2142      *
2143      * @param int $repositoryid
2144      * @param string $reference
2145      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2146      * @return int|bool
2147      */
2148     private function get_referencefileid($repositoryid, $reference, $strictness) {
2149         global $DB;
2151         return $DB->get_field('files_reference', 'id',
2152             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2153     }
2155     /**
2156      * Updates a reference to the external resource and all files that use it
2157      *
2158      * This function is called after synchronisation of an external file and updates the
2159      * contenthash, filesize and status of all files that reference this external file
2160      * as well as time last synchronised and sync lifetime (how long we don't need to call
2161      * synchronisation for this reference).
2162      *
2163      * @param int $referencefileid
2164      * @param int $lastsync
2165      * @param int $lifetime
2166      * @param string $contenthash
2167      * @param int $filesize
2168      * @param int $status 0 if ok or 666 if source is missing
2169      */
2170     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2171         global $DB;
2172         $referencefileid = clean_param($referencefileid, PARAM_INT);
2173         $lastsync = clean_param($lastsync, PARAM_INT);
2174         $lifetime = clean_param($lifetime, PARAM_INT);
2175         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2176         $filesize = clean_param($filesize, PARAM_INT);
2177         $status = clean_param($status, PARAM_INT);
2178         $params = array('contenthash' => $contenthash,
2179                     'filesize' => $filesize,
2180                     'status' => $status,
2181                     'referencefileid' => $referencefileid,
2182                     'lastsync' => $lastsync,
2183                     'lifetime' => $lifetime);
2184         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2185             status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
2186             WHERE referencefileid = :referencefileid', $params);
2187         $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2188         $DB->update_record('files_reference', (object)$data);
2189     }