Merge branch 'wip-MDL-33356-master' of git://github.com/marinaglancy/moodle
[moodle.git] / lib / filestorage / file_storage.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Core file storage class definition.
20  *
21  * @package   core_files
22  * @copyright 2008 Petr Skoda {@link http://skodak.org}
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/stored_file.php");
30 /**
31  * File storage class used for low level access to stored files.
32  *
33  * Only owner of file area may use this class to access own files,
34  * for example only code in mod/assignment/* may access assignment
35  * attachments. When some other part of moodle needs to access
36  * files of modules it has to use file_browser class instead or there
37  * has to be some callback API.
38  *
39  * @package   core_files
40  * @category  files
41  * @copyright 2008 Petr Skoda {@link http://skodak.org}
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  * @since     Moodle 2.0
44  */
45 class file_storage {
46     /** @var string Directory with file contents */
47     private $filedir;
48     /** @var string Contents of deleted files not needed any more */
49     private $trashdir;
50     /** @var string tempdir */
51     private $tempdir;
52     /** @var int Permissions for new directories */
53     private $dirpermissions;
54     /** @var int Permissions for new files */
55     private $filepermissions;
57     /**
58      * Constructor - do not use directly use {@link get_file_storage()} call instead.
59      *
60      * @param string $filedir full path to pool directory
61      * @param string $trashdir temporary storage of deleted area
62      * @param string $tempdir temporary storage of various files
63      * @param int $dirpermissions new directory permissions
64      * @param int $filepermissions new file permissions
65      */
66     public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
67         $this->filedir         = $filedir;
68         $this->trashdir        = $trashdir;
69         $this->tempdir         = $tempdir;
70         $this->dirpermissions  = $dirpermissions;
71         $this->filepermissions = $filepermissions;
73         // make sure the file pool directory exists
74         if (!is_dir($this->filedir)) {
75             if (!mkdir($this->filedir, $this->dirpermissions, true)) {
76                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
77             }
78             // place warning file in file pool root
79             if (!file_exists($this->filedir.'/warning.txt')) {
80                 file_put_contents($this->filedir.'/warning.txt',
81                                   'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
82             }
83         }
84         // make sure the file pool directory exists
85         if (!is_dir($this->trashdir)) {
86             if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
87                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
88             }
89         }
90     }
92     /**
93      * Calculates sha1 hash of unique full path name information.
94      *
95      * This hash is a unique file identifier - it is used to improve
96      * performance and overcome db index size limits.
97      *
98      * @param int $contextid context ID
99      * @param string $component component
100      * @param string $filearea file area
101      * @param int $itemid item ID
102      * @param string $filepath file path
103      * @param string $filename file name
104      * @return string sha1 hash
105      */
106     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
108     }
110     /**
111      * Does this file exist?
112      *
113      * @param int $contextid context ID
114      * @param string $component component
115      * @param string $filearea file area
116      * @param int $itemid item ID
117      * @param string $filepath file path
118      * @param string $filename file name
119      * @return bool
120      */
121     public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
122         $filepath = clean_param($filepath, PARAM_PATH);
123         $filename = clean_param($filename, PARAM_FILE);
125         if ($filename === '') {
126             $filename = '.';
127         }
129         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
130         return $this->file_exists_by_hash($pathnamehash);
131     }
133     /**
134      * Whether or not the file exist
135      *
136      * @param string $pathnamehash path name hash
137      * @return bool
138      */
139     public function file_exists_by_hash($pathnamehash) {
140         global $DB;
142         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
143     }
145     /**
146      * Create instance of file class from database record.
147      *
148      * @param stdClass $filerecord record from the files table left join files_reference table
149      * @return stored_file instance of file abstraction class
150      */
151     public function get_file_instance(stdClass $filerecord) {
152         $storedfile = new stored_file($this, $filerecord, $this->filedir);
153         return $storedfile;
154     }
156     /**
157      * Returns an image file that represent the given stored file as a preview
158      *
159      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
160      * future, the support for other mimetypes can be added, too (eg. generate an image
161      * preview of PDF, text documents etc).
162      *
163      * @param stored_file $file the file we want to preview
164      * @param string $mode preview mode, eg. 'thumb'
165      * @return stored_file|bool false if unable to create the preview, stored file otherwise
166      */
167     public function get_file_preview(stored_file $file, $mode) {
169         $context = context_system::instance();
170         $path = '/' . trim($mode, '/') . '/';
171         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
173         if (!$preview) {
174             $preview = $this->create_file_preview($file, $mode);
175             if (!$preview) {
176                 return false;
177             }
178         }
180         return $preview;
181     }
183     /**
184      * Generates a preview image for the stored file
185      *
186      * @param stored_file $file the file we want to preview
187      * @param string $mode preview mode, eg. 'thumb'
188      * @return stored_file|bool the newly created preview file or false
189      */
190     protected function create_file_preview(stored_file $file, $mode) {
192         $mimetype = $file->get_mimetype();
194         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
195             // make a preview of the image
196             $data = $this->create_imagefile_preview($file, $mode);
198         } else {
199             // unable to create the preview of this mimetype yet
200             return false;
201         }
203         if (empty($data)) {
204             return false;
205         }
207         // getimagesizefromstring() is available from PHP 5.4 but we need to support
208         // lower versions, so...
209         $tmproot = make_temp_directory('thumbnails');
210         $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
211         file_put_contents($tmpfilepath, $data);
212         $imageinfo = getimagesize($tmpfilepath);
213         unlink($tmpfilepath);
215         $context = context_system::instance();
217         $record = array(
218             'contextid' => $context->id,
219             'component' => 'core',
220             'filearea'  => 'preview',
221             'itemid'    => 0,
222             'filepath'  => '/' . trim($mode, '/') . '/',
223             'filename'  => $file->get_contenthash(),
224         );
226         if ($imageinfo) {
227             $record['mimetype'] = $imageinfo['mime'];
228         }
230         return $this->create_file_from_string($record, $data);
231     }
233     /**
234      * Generates a preview for the stored image file
235      *
236      * @param stored_file $file the image we want to preview
237      * @param string $mode preview mode, eg. 'thumb'
238      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
239      */
240     protected function create_imagefile_preview(stored_file $file, $mode) {
241         global $CFG;
242         require_once($CFG->libdir.'/gdlib.php');
244         $tmproot = make_temp_directory('thumbnails');
245         $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
246         $file->copy_content_to($tmpfilepath);
248         if ($mode === 'tinyicon') {
249             $data = generate_image_thumbnail($tmpfilepath, 24, 24);
251         } else if ($mode === 'thumb') {
252             $data = generate_image_thumbnail($tmpfilepath, 90, 90);
254         } else {
255             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
256         }
258         unlink($tmpfilepath);
260         return $data;
261     }
263     /**
264      * Fetch file using local file id.
265      *
266      * Please do not rely on file ids, it is usually easier to use
267      * pathname hashes instead.
268      *
269      * @param int $fileid file ID
270      * @return stored_file|bool stored_file instance if exists, false if not
271      */
272     public function get_file_by_id($fileid) {
273         global $DB;
275         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
276                   FROM {files} f
277              LEFT JOIN {files_reference} r
278                        ON f.referencefileid = r.id
279                  WHERE f.id = ?";
280         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
281             return $this->get_file_instance($filerecord);
282         } else {
283             return false;
284         }
285     }
287     /**
288      * Fetch file using local file full pathname hash
289      *
290      * @param string $pathnamehash path name hash
291      * @return stored_file|bool stored_file instance if exists, false if not
292      */
293     public function get_file_by_hash($pathnamehash) {
294         global $DB;
296         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
297                   FROM {files} f
298              LEFT JOIN {files_reference} r
299                        ON f.referencefileid = r.id
300                  WHERE f.pathnamehash = ?";
301         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
302             return $this->get_file_instance($filerecord);
303         } else {
304             return false;
305         }
306     }
308     /**
309      * Fetch locally stored file.
310      *
311      * @param int $contextid context ID
312      * @param string $component component
313      * @param string $filearea file area
314      * @param int $itemid item ID
315      * @param string $filepath file path
316      * @param string $filename file name
317      * @return stored_file|bool stored_file instance if exists, false if not
318      */
319     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
320         $filepath = clean_param($filepath, PARAM_PATH);
321         $filename = clean_param($filename, PARAM_FILE);
323         if ($filename === '') {
324             $filename = '.';
325         }
327         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
328         return $this->get_file_by_hash($pathnamehash);
329     }
331     /**
332      * Are there any files (or directories)
333      *
334      * @param int $contextid context ID
335      * @param string $component component
336      * @param string $filearea file area
337      * @param bool|int $itemid item id or false if all items
338      * @param bool $ignoredirs whether or not ignore directories
339      * @return bool empty
340      */
341     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
342         global $DB;
344         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
345         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
347         if ($itemid !== false) {
348             $params['itemid'] = $itemid;
349             $where .= " AND itemid = :itemid";
350         }
352         if ($ignoredirs) {
353             $sql = "SELECT 'x'
354                       FROM {files}
355                      WHERE $where AND filename <> '.'";
356         } else {
357             $sql = "SELECT 'x'
358                       FROM {files}
359                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
360         }
362         return !$DB->record_exists_sql($sql, $params);
363     }
365     /**
366      * Returns all files belonging to given repository
367      *
368      * @param int $repositoryid
369      * @param string $sort
370      */
371     public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
372         global $DB;
373         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
374                   FROM {files} f
375              LEFT JOIN {files_reference} r
376                        ON f.referencefileid = r.id
377                  WHERE r.repositoryid = ?
378               ORDER BY $sort";
380         $result = array();
381         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
382         foreach ($filerecords as $filerecord) {
383             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
384         }
385         return $result;
386     }
388     /**
389      * Returns all area files (optionally limited by itemid)
390      *
391      * @param int $contextid context ID
392      * @param string $component component
393      * @param string $filearea file area
394      * @param int $itemid item ID or all files if not specified
395      * @param string $sort sort fields
396      * @param bool $includedirs whether or not include directories
397      * @return array of stored_files indexed by pathanmehash
398      */
399     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
400         global $DB;
402         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
403         if ($itemid !== false) {
404             $itemidsql = ' AND f.itemid = :itemid ';
405             $conditions['itemid'] = $itemid;
406         } else {
407             $itemidsql = '';
408         }
410         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
411                   FROM {files} f
412              LEFT JOIN {files_reference} r
413                        ON f.referencefileid = r.id
414                  WHERE f.contextid = :contextid
415                        AND f.component = :component
416                        AND f.filearea = :filearea
417                        $itemidsql
418               ORDER BY $sort";
420         $result = array();
421         $filerecords = $DB->get_records_sql($sql, $conditions);
422         foreach ($filerecords as $filerecord) {
423             if (!$includedirs and $filerecord->filename === '.') {
424                 continue;
425             }
426             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
427         }
428         return $result;
429     }
431     /**
432      * Returns array based tree structure of area files
433      *
434      * @param int $contextid context ID
435      * @param string $component component
436      * @param string $filearea file area
437      * @param int $itemid item ID
438      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
439      */
440     public function get_area_tree($contextid, $component, $filearea, $itemid) {
441         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
442         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
443         // first create directory structure
444         foreach ($files as $hash=>$dir) {
445             if (!$dir->is_directory()) {
446                 continue;
447             }
448             unset($files[$hash]);
449             if ($dir->get_filepath() === '/') {
450                 $result['dirfile'] = $dir;
451                 continue;
452             }
453             $parts = explode('/', trim($dir->get_filepath(),'/'));
454             $pointer =& $result;
455             foreach ($parts as $part) {
456                 if ($part === '') {
457                     continue;
458                 }
459                 if (!isset($pointer['subdirs'][$part])) {
460                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
461                 }
462                 $pointer =& $pointer['subdirs'][$part];
463             }
464             $pointer['dirfile'] = $dir;
465             unset($pointer);
466         }
467         foreach ($files as $hash=>$file) {
468             $parts = explode('/', trim($file->get_filepath(),'/'));
469             $pointer =& $result;
470             foreach ($parts as $part) {
471                 if ($part === '') {
472                     continue;
473                 }
474                 $pointer =& $pointer['subdirs'][$part];
475             }
476             $pointer['files'][$file->get_filename()] = $file;
477             unset($pointer);
478         }
479         return $result;
480     }
482     /**
483      * Returns all files and optionally directories
484      *
485      * @param int $contextid context ID
486      * @param string $component component
487      * @param string $filearea file area
488      * @param int $itemid item ID
489      * @param int $filepath directory path
490      * @param bool $recursive include all subdirectories
491      * @param bool $includedirs include files and directories
492      * @param string $sort sort fields
493      * @return array of stored_files indexed by pathanmehash
494      */
495     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
496         global $DB;
498         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
499             return array();
500         }
502         if ($recursive) {
504             $dirs = $includedirs ? "" : "AND filename <> '.'";
505             $length = textlib::strlen($filepath);
507             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
508                       FROM {files} f
509                  LEFT JOIN {files_reference} r
510                            ON f.referencefileid = r.id
511                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
512                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
513                            AND f.id <> :dirid
514                            $dirs
515                   ORDER BY $sort";
516             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
518             $files = array();
519             $dirs  = array();
520             $filerecords = $DB->get_records_sql($sql, $params);
521             foreach ($filerecords as $filerecord) {
522                 if ($filerecord->filename == '.') {
523                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
524                 } else {
525                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
526                 }
527             }
528             $result = array_merge($dirs, $files);
530         } else {
531             $result = array();
532             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
534             $length = textlib::strlen($filepath);
536             if ($includedirs) {
537                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
538                           FROM {files} f
539                      LEFT JOIN {files_reference} r
540                                ON f.referencefileid = r.id
541                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
542                                AND f.itemid = :itemid AND f.filename = '.'
543                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
544                                AND f.id <> :dirid
545                       ORDER BY $sort";
546                 $reqlevel = substr_count($filepath, '/') + 1;
547                 $filerecords = $DB->get_records_sql($sql, $params);
548                 foreach ($filerecords as $filerecord) {
549                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
550                         continue;
551                     }
552                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
553                 }
554             }
556             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
557                       FROM {files} f
558                  LEFT JOIN {files_reference} r
559                            ON f.referencefileid = r.id
560                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
561                            AND f.filepath = :filepath AND f.filename <> '.'
562                   ORDER BY $sort";
564             $filerecords = $DB->get_records_sql($sql, $params);
565             foreach ($filerecords as $filerecord) {
566                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
567             }
568         }
570         return $result;
571     }
573     /**
574      * Delete all area files (optionally limited by itemid).
575      *
576      * @param int $contextid context ID
577      * @param string $component component
578      * @param string $filearea file area or all areas in context if not specified
579      * @param int $itemid item ID or all files if not specified
580      * @return bool success
581      */
582     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
583         global $DB;
585         $conditions = array('contextid'=>$contextid);
586         if ($component !== false) {
587             $conditions['component'] = $component;
588         }
589         if ($filearea !== false) {
590             $conditions['filearea'] = $filearea;
591         }
592         if ($itemid !== false) {
593             $conditions['itemid'] = $itemid;
594         }
596         $filerecords = $DB->get_records('files', $conditions);
597         foreach ($filerecords as $filerecord) {
598             $this->get_file_instance($filerecord)->delete();
599         }
601         return true; // BC only
602     }
604     /**
605      * Delete all the files from certain areas where itemid is limited by an
606      * arbitrary bit of SQL.
607      *
608      * @param int $contextid the id of the context the files belong to. Must be given.
609      * @param string $component the owning component. Must be given.
610      * @param string $filearea the file area name. Must be given.
611      * @param string $itemidstest an SQL fragment that the itemid must match. Used
612      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
613      *      and may not used named parameters called contextid, component or filearea.
614      * @param array $params any query params used by $itemidstest.
615      */
616     public function delete_area_files_select($contextid, $component,
617             $filearea, $itemidstest, array $params = null) {
618         global $DB;
620         $where = "contextid = :contextid
621                 AND component = :component
622                 AND filearea = :filearea
623                 AND itemid $itemidstest";
624         $params['contextid'] = $contextid;
625         $params['component'] = $component;
626         $params['filearea'] = $filearea;
628         $filerecords = $DB->get_recordset_select('files', $where, $params);
629         foreach ($filerecords as $filerecord) {
630             $this->get_file_instance($filerecord)->delete();
631         }
632         $filerecords->close();
633     }
635     /**
636      * Move all the files in a file area from one context to another.
637      *
638      * @param int $oldcontextid the context the files are being moved from.
639      * @param int $newcontextid the context the files are being moved to.
640      * @param string $component the plugin that these files belong to.
641      * @param string $filearea the name of the file area.
642      * @param int $itemid file item ID
643      * @return int the number of files moved, for information.
644      */
645     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
646         // Note, this code is based on some code that Petr wrote in
647         // forum_move_attachments in mod/forum/lib.php. I moved it here because
648         // I needed it in the question code too.
649         $count = 0;
651         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
652         foreach ($oldfiles as $oldfile) {
653             $filerecord = new stdClass();
654             $filerecord->contextid = $newcontextid;
655             $this->create_file_from_storedfile($filerecord, $oldfile);
656             $count += 1;
657         }
659         if ($count) {
660             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
661         }
663         return $count;
664     }
666     /**
667      * Recursively creates directory.
668      *
669      * @param int $contextid context ID
670      * @param string $component component
671      * @param string $filearea file area
672      * @param int $itemid item ID
673      * @param string $filepath file path
674      * @param int $userid the user ID
675      * @return bool success
676      */
677     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
678         global $DB;
680         // validate all parameters, we do not want any rubbish stored in database, right?
681         if (!is_number($contextid) or $contextid < 1) {
682             throw new file_exception('storedfileproblem', 'Invalid contextid');
683         }
685         $component = clean_param($component, PARAM_COMPONENT);
686         if (empty($component)) {
687             throw new file_exception('storedfileproblem', 'Invalid component');
688         }
690         $filearea = clean_param($filearea, PARAM_AREA);
691         if (empty($filearea)) {
692             throw new file_exception('storedfileproblem', 'Invalid filearea');
693         }
695         if (!is_number($itemid) or $itemid < 0) {
696             throw new file_exception('storedfileproblem', 'Invalid itemid');
697         }
699         $filepath = clean_param($filepath, PARAM_PATH);
700         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
701             // path must start and end with '/'
702             throw new file_exception('storedfileproblem', 'Invalid file path');
703         }
705         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
707         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
708             return $dir_info;
709         }
711         static $contenthash = null;
712         if (!$contenthash) {
713             $this->add_string_to_pool('');
714             $contenthash = sha1('');
715         }
717         $now = time();
719         $dir_record = new stdClass();
720         $dir_record->contextid = $contextid;
721         $dir_record->component = $component;
722         $dir_record->filearea  = $filearea;
723         $dir_record->itemid    = $itemid;
724         $dir_record->filepath  = $filepath;
725         $dir_record->filename  = '.';
726         $dir_record->contenthash  = $contenthash;
727         $dir_record->filesize  = 0;
729         $dir_record->timecreated  = $now;
730         $dir_record->timemodified = $now;
731         $dir_record->mimetype     = null;
732         $dir_record->userid       = $userid;
734         $dir_record->pathnamehash = $pathnamehash;
736         $DB->insert_record('files', $dir_record);
737         $dir_info = $this->get_file_by_hash($pathnamehash);
739         if ($filepath !== '/') {
740             //recurse to parent dirs
741             $filepath = trim($filepath, '/');
742             $filepath = explode('/', $filepath);
743             array_pop($filepath);
744             $filepath = implode('/', $filepath);
745             $filepath = ($filepath === '') ? '/' : "/$filepath/";
746             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
747         }
749         return $dir_info;
750     }
752     /**
753      * Add new local file based on existing local file.
754      *
755      * @param stdClass|array $filerecord object or array describing changes
756      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
757      * @return stored_file instance of newly created file
758      */
759     public function create_file_from_storedfile($filerecord, $fileorid) {
760         global $DB;
762         if ($fileorid instanceof stored_file) {
763             $fid = $fileorid->get_id();
764         } else {
765             $fid = $fileorid;
766         }
768         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
770         unset($filerecord['id']);
771         unset($filerecord['filesize']);
772         unset($filerecord['contenthash']);
773         unset($filerecord['pathnamehash']);
775         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
776                   FROM {files} f
777              LEFT JOIN {files_reference} r
778                        ON f.referencefileid = r.id
779                  WHERE f.id = ?";
781         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
782             throw new file_exception('storedfileproblem', 'File does not exist');
783         }
785         unset($newrecord->id);
787         foreach ($filerecord as $key => $value) {
788             // validate all parameters, we do not want any rubbish stored in database, right?
789             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
790                 throw new file_exception('storedfileproblem', 'Invalid contextid');
791             }
793             if ($key == 'component') {
794                 $value = clean_param($value, PARAM_COMPONENT);
795                 if (empty($value)) {
796                     throw new file_exception('storedfileproblem', 'Invalid component');
797                 }
798             }
800             if ($key == 'filearea') {
801                 $value = clean_param($value, PARAM_AREA);
802                 if (empty($value)) {
803                     throw new file_exception('storedfileproblem', 'Invalid filearea');
804                 }
805             }
807             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
808                 throw new file_exception('storedfileproblem', 'Invalid itemid');
809             }
812             if ($key == 'filepath') {
813                 $value = clean_param($value, PARAM_PATH);
814                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
815                     // path must start and end with '/'
816                     throw new file_exception('storedfileproblem', 'Invalid file path');
817                 }
818             }
820             if ($key == 'filename') {
821                 $value = clean_param($value, PARAM_FILE);
822                 if ($value === '') {
823                     // path must start and end with '/'
824                     throw new file_exception('storedfileproblem', 'Invalid file name');
825                 }
826             }
828             if ($key === 'timecreated' or $key === 'timemodified') {
829                 if (!is_number($value)) {
830                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
831                 }
832                 if ($value < 0) {
833                     //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)
834                     $value = 0;
835                 }
836             }
838             if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
839                 $value = clean_param($value, PARAM_INT);
840             }
842             $newrecord->$key = $value;
843         }
845         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
847         if ($newrecord->filename === '.') {
848             // special case - only this function supports directories ;-)
849             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
850             // update the existing directory with the new data
851             $newrecord->id = $directory->get_id();
852             $DB->update_record('files', $newrecord);
853             return $this->get_file_instance($newrecord);
854         }
856         if (!empty($newrecord->repositoryid)) {
857             try {
858                 $referencerecord = new stdClass;
859                 $referencerecord->repositoryid = $newrecord->repositoryid;
860                 $referencerecord->reference = $newrecord->reference;
861                 $referencerecord->lastsync  = $newrecord->referencelastsync;
862                 $referencerecord->lifetime  = $newrecord->referencelifetime;
863                 $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
864             } catch (dml_exception $e) {
865                 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
866                                                          $newrecord->filepath, $newrecord->filename, $e->debuginfo);
867             }
868             $newrecord->referencefileid = $referencerecord->id;
869         }
871         try {
872             $newrecord->id = $DB->insert_record('files', $newrecord);
873         } catch (dml_exception $e) {
874             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
875                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
876         }
879         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
881         return $this->get_file_instance($newrecord);
882     }
884     /**
885      * Add new local file.
886      *
887      * @param stdClass|array $filerecord object or array describing file
888      * @param string $url the URL to the file
889      * @param array $options {@link download_file_content()} options
890      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
891      * @return stored_file
892      */
893     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
895         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
896         $filerecord = (object)$filerecord; // We support arrays too.
898         $headers        = isset($options['headers'])        ? $options['headers'] : null;
899         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
900         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
901         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
902         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
903         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
904         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
906         if (!isset($filerecord->filename)) {
907             $parts = explode('/', $url);
908             $filename = array_pop($parts);
909             $filerecord->filename = clean_param($filename, PARAM_FILE);
910         }
911         $source = !empty($filerecord->source) ? $filerecord->source : $url;
912         $filerecord->source = clean_param($source, PARAM_URL);
914         if ($usetempfile) {
915             check_dir_exists($this->tempdir);
916             $tmpfile = tempnam($this->tempdir, 'newfromurl');
917             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
918             if ($content === false) {
919                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
920             }
921             try {
922                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
923                 @unlink($tmpfile);
924                 return $newfile;
925             } catch (Exception $e) {
926                 @unlink($tmpfile);
927                 throw $e;
928             }
930         } else {
931             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
932             if ($content === false) {
933                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
934             }
935             return $this->create_file_from_string($filerecord, $content);
936         }
937     }
939     /**
940      * Add new local file.
941      *
942      * @param stdClass|array $filerecord object or array describing file
943      * @param string $pathname path to file or content of file
944      * @return stored_file
945      */
946     public function create_file_from_pathname($filerecord, $pathname) {
947         global $DB;
949         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
950         $filerecord = (object)$filerecord; // We support arrays too.
952         // validate all parameters, we do not want any rubbish stored in database, right?
953         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
954             throw new file_exception('storedfileproblem', 'Invalid contextid');
955         }
957         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
958         if (empty($filerecord->component)) {
959             throw new file_exception('storedfileproblem', 'Invalid component');
960         }
962         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
963         if (empty($filerecord->filearea)) {
964             throw new file_exception('storedfileproblem', 'Invalid filearea');
965         }
967         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
968             throw new file_exception('storedfileproblem', 'Invalid itemid');
969         }
971         if (!empty($filerecord->sortorder)) {
972             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
973                 $filerecord->sortorder = 0;
974             }
975         } else {
976             $filerecord->sortorder = 0;
977         }
979         $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
980         $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
981         $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
983         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
984         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
985             // path must start and end with '/'
986             throw new file_exception('storedfileproblem', 'Invalid file path');
987         }
989         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
990         if ($filerecord->filename === '') {
991             // filename must not be empty
992             throw new file_exception('storedfileproblem', 'Invalid file name');
993         }
995         $now = time();
996         if (isset($filerecord->timecreated)) {
997             if (!is_number($filerecord->timecreated)) {
998                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
999             }
1000             if ($filerecord->timecreated < 0) {
1001                 //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)
1002                 $filerecord->timecreated = 0;
1003             }
1004         } else {
1005             $filerecord->timecreated = $now;
1006         }
1008         if (isset($filerecord->timemodified)) {
1009             if (!is_number($filerecord->timemodified)) {
1010                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1011             }
1012             if ($filerecord->timemodified < 0) {
1013                 //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)
1014                 $filerecord->timemodified = 0;
1015             }
1016         } else {
1017             $filerecord->timemodified = $now;
1018         }
1020         $newrecord = new stdClass();
1022         $newrecord->contextid = $filerecord->contextid;
1023         $newrecord->component = $filerecord->component;
1024         $newrecord->filearea  = $filerecord->filearea;
1025         $newrecord->itemid    = $filerecord->itemid;
1026         $newrecord->filepath  = $filerecord->filepath;
1027         $newrecord->filename  = $filerecord->filename;
1029         $newrecord->timecreated  = $filerecord->timecreated;
1030         $newrecord->timemodified = $filerecord->timemodified;
1031         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname) : $filerecord->mimetype;
1032         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1033         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1034         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1035         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1036         $newrecord->sortorder    = $filerecord->sortorder;
1038         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1040         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1042         try {
1043             $newrecord->id = $DB->insert_record('files', $newrecord);
1044         } catch (dml_exception $e) {
1045             if ($newfile) {
1046                 $this->deleted_file_cleanup($newrecord->contenthash);
1047             }
1048             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1049                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1050         }
1052         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1054         return $this->get_file_instance($newrecord);
1055     }
1057     /**
1058      * Add new local file.
1059      *
1060      * @param stdClass|array $filerecord object or array describing file
1061      * @param string $content content of file
1062      * @return stored_file
1063      */
1064     public function create_file_from_string($filerecord, $content) {
1065         global $DB;
1067         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1068         $filerecord = (object)$filerecord; // We support arrays too.
1070         // validate all parameters, we do not want any rubbish stored in database, right?
1071         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1072             throw new file_exception('storedfileproblem', 'Invalid contextid');
1073         }
1075         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1076         if (empty($filerecord->component)) {
1077             throw new file_exception('storedfileproblem', 'Invalid component');
1078         }
1080         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1081         if (empty($filerecord->filearea)) {
1082             throw new file_exception('storedfileproblem', 'Invalid filearea');
1083         }
1085         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1086             throw new file_exception('storedfileproblem', 'Invalid itemid');
1087         }
1089         if (!empty($filerecord->sortorder)) {
1090             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1091                 $filerecord->sortorder = 0;
1092             }
1093         } else {
1094             $filerecord->sortorder = 0;
1095         }
1096         $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
1097         $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
1098         $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
1100         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1101         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1102             // path must start and end with '/'
1103             throw new file_exception('storedfileproblem', 'Invalid file path');
1104         }
1106         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1107         if ($filerecord->filename === '') {
1108             // path must start and end with '/'
1109             throw new file_exception('storedfileproblem', 'Invalid file name');
1110         }
1112         $now = time();
1113         if (isset($filerecord->timecreated)) {
1114             if (!is_number($filerecord->timecreated)) {
1115                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1116             }
1117             if ($filerecord->timecreated < 0) {
1118                 //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)
1119                 $filerecord->timecreated = 0;
1120             }
1121         } else {
1122             $filerecord->timecreated = $now;
1123         }
1125         if (isset($filerecord->timemodified)) {
1126             if (!is_number($filerecord->timemodified)) {
1127                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1128             }
1129             if ($filerecord->timemodified < 0) {
1130                 //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)
1131                 $filerecord->timemodified = 0;
1132             }
1133         } else {
1134             $filerecord->timemodified = $now;
1135         }
1137         $newrecord = new stdClass();
1139         $newrecord->contextid = $filerecord->contextid;
1140         $newrecord->component = $filerecord->component;
1141         $newrecord->filearea  = $filerecord->filearea;
1142         $newrecord->itemid    = $filerecord->itemid;
1143         $newrecord->filepath  = $filerecord->filepath;
1144         $newrecord->filename  = $filerecord->filename;
1146         $newrecord->timecreated  = $filerecord->timecreated;
1147         $newrecord->timemodified = $filerecord->timemodified;
1148         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1149         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1150         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1151         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1152         $newrecord->sortorder    = $filerecord->sortorder;
1154         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1155         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1156         // get mimetype by magic bytes
1157         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname) : $filerecord->mimetype;
1159         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1161         try {
1162             $newrecord->id = $DB->insert_record('files', $newrecord);
1163         } catch (dml_exception $e) {
1164             if ($newfile) {
1165                 $this->deleted_file_cleanup($newrecord->contenthash);
1166             }
1167             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1168                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1169         }
1171         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1173         return $this->get_file_instance($newrecord);
1174     }
1176     /**
1177      * Create a moodle file from file reference information
1178      *
1179      * @param stdClass $filerecord
1180      * @param int $repositoryid
1181      * @param string $reference
1182      * @param array $options options for creating external file
1183      * @return stored_file
1184      */
1185     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1186         global $DB;
1188         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1189         $filerecord = (object)$filerecord; // We support arrays too.
1191         // validate all parameters, we do not want any rubbish stored in database, right?
1192         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1193             throw new file_exception('storedfileproblem', 'Invalid contextid');
1194         }
1196         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1197         if (empty($filerecord->component)) {
1198             throw new file_exception('storedfileproblem', 'Invalid component');
1199         }
1201         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1202         if (empty($filerecord->filearea)) {
1203             throw new file_exception('storedfileproblem', 'Invalid filearea');
1204         }
1206         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1207             throw new file_exception('storedfileproblem', 'Invalid itemid');
1208         }
1210         if (!empty($filerecord->sortorder)) {
1211             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1212                 $filerecord->sortorder = 0;
1213             }
1214         } else {
1215             $filerecord->sortorder = 0;
1216         }
1218         $filerecord->referencefileid   = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
1219         $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
1220         $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
1221         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1222         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1223         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1224         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1225         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1226         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1227         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1228             // Path must start and end with '/'.
1229             throw new file_exception('storedfileproblem', 'Invalid file path');
1230         }
1232         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1233         if ($filerecord->filename === '') {
1234             // Path must start and end with '/'.
1235             throw new file_exception('storedfileproblem', 'Invalid file name');
1236         }
1238         $now = time();
1239         if (isset($filerecord->timecreated)) {
1240             if (!is_number($filerecord->timecreated)) {
1241                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1242             }
1243             if ($filerecord->timecreated < 0) {
1244                 // 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)
1245                 $filerecord->timecreated = 0;
1246             }
1247         } else {
1248             $filerecord->timecreated = $now;
1249         }
1251         if (isset($filerecord->timemodified)) {
1252             if (!is_number($filerecord->timemodified)) {
1253                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1254             }
1255             if ($filerecord->timemodified < 0) {
1256                 // 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)
1257                 $filerecord->timemodified = 0;
1258             }
1259         } else {
1260             $filerecord->timemodified = $now;
1261         }
1263         $transaction = $DB->start_delegated_transaction();
1265         // Insert file reference record.
1266         try {
1267             $referencerecord = new stdClass;
1268             $referencerecord->repositoryid = $repositoryid;
1269             $referencerecord->reference = $reference;
1270             $referencerecord->lastsync = $filerecord->referencelastsync;
1271             $referencerecord->lifetime = $filerecord->referencelifetime;
1272             $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
1273         } catch (dml_exception $e) {
1274             throw $e;
1275         }
1277         $filerecord->referencefileid = $referencerecord->id;
1279         // External file doesn't have content in moodle.
1280         // So we create an empty file for it.
1281         list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1283         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1285         try {
1286             $filerecord->id = $DB->insert_record('files', $filerecord);
1287         } catch (dml_exception $e) {
1288             if ($newfile) {
1289                 $this->deleted_file_cleanup($filerecord->contenthash);
1290             }
1291             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1292                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1293         }
1295         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1297         $transaction->allow_commit();
1299         // Adding repositoryid and reference to file record to create stored_file instance
1300         $filerecord->repositoryid = $repositoryid;
1301         $filerecord->reference = $reference;
1302         return $this->get_file_instance($filerecord);
1303     }
1305     /**
1306      * Creates new image file from existing.
1307      *
1308      * @param stdClass|array $filerecord object or array describing new file
1309      * @param int|stored_file $fid file id or stored file object
1310      * @param int $newwidth in pixels
1311      * @param int $newheight in pixels
1312      * @param bool $keepaspectratio whether or not keep aspect ratio
1313      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1314      * @return stored_file
1315      */
1316     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1317         if (!function_exists('imagecreatefromstring')) {
1318             //Most likely the GD php extension isn't installed
1319             //image conversion cannot succeed
1320             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1321         }
1323         if ($fid instanceof stored_file) {
1324             $fid = $fid->get_id();
1325         }
1327         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1329         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1330             throw new file_exception('storedfileproblem', 'File does not exist');
1331         }
1333         if (!$imageinfo = $file->get_imageinfo()) {
1334             throw new file_exception('storedfileproblem', 'File is not an image');
1335         }
1337         if (!isset($filerecord['filename'])) {
1338             $filerecord['filename'] = $file->get_filename();
1339         }
1341         if (!isset($filerecord['mimetype'])) {
1342             $filerecord['mimetype'] = $imageinfo['mimetype'];
1343         }
1345         $width    = $imageinfo['width'];
1346         $height   = $imageinfo['height'];
1347         $mimetype = $imageinfo['mimetype'];
1349         if ($keepaspectratio) {
1350             if (0 >= $newwidth and 0 >= $newheight) {
1351                 // no sizes specified
1352                 $newwidth  = $width;
1353                 $newheight = $height;
1355             } else if (0 < $newwidth and 0 < $newheight) {
1356                 $xheight = ($newwidth*($height/$width));
1357                 if ($xheight < $newheight) {
1358                     $newheight = (int)$xheight;
1359                 } else {
1360                     $newwidth = (int)($newheight*($width/$height));
1361                 }
1363             } else if (0 < $newwidth) {
1364                 $newheight = (int)($newwidth*($height/$width));
1366             } else { //0 < $newheight
1367                 $newwidth = (int)($newheight*($width/$height));
1368             }
1370         } else {
1371             if (0 >= $newwidth) {
1372                 $newwidth = $width;
1373             }
1374             if (0 >= $newheight) {
1375                 $newheight = $height;
1376             }
1377         }
1379         $img = imagecreatefromstring($file->get_content());
1380         if ($height != $newheight or $width != $newwidth) {
1381             $newimg = imagecreatetruecolor($newwidth, $newheight);
1382             if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1383                 // weird
1384                 throw new file_exception('storedfileproblem', 'Can not resize image');
1385             }
1386             imagedestroy($img);
1387             $img = $newimg;
1388         }
1390         ob_start();
1391         switch ($filerecord['mimetype']) {
1392             case 'image/gif':
1393                 imagegif($img);
1394                 break;
1396             case 'image/jpeg':
1397                 if (is_null($quality)) {
1398                     imagejpeg($img);
1399                 } else {
1400                     imagejpeg($img, NULL, $quality);
1401                 }
1402                 break;
1404             case 'image/png':
1405                 $quality = (int)$quality;
1406                 imagepng($img, NULL, $quality, NULL);
1407                 break;
1409             default:
1410                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1411         }
1413         $content = ob_get_contents();
1414         ob_end_clean();
1415         imagedestroy($img);
1417         if (!$content) {
1418             throw new file_exception('storedfileproblem', 'Can not convert image');
1419         }
1421         return $this->create_file_from_string($filerecord, $content);
1422     }
1424     /**
1425      * Add file content to sha1 pool.
1426      *
1427      * @param string $pathname path to file
1428      * @param string $contenthash sha1 hash of content if known (performance only)
1429      * @return array (contenthash, filesize, newfile)
1430      */
1431     public function add_file_to_pool($pathname, $contenthash = NULL) {
1432         if (!is_readable($pathname)) {
1433             throw new file_exception('storedfilecannotread', '', $pathname);
1434         }
1436         if (is_null($contenthash)) {
1437             $contenthash = sha1_file($pathname);
1438         }
1440         $filesize = filesize($pathname);
1442         $hashpath = $this->path_from_hash($contenthash);
1443         $hashfile = "$hashpath/$contenthash";
1445         if (file_exists($hashfile)) {
1446             if (filesize($hashfile) !== $filesize) {
1447                 throw new file_pool_content_exception($contenthash);
1448             }
1449             $newfile = false;
1451         } else {
1452             if (!is_dir($hashpath)) {
1453                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1454                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1455                 }
1456             }
1457             $newfile = true;
1459             if (!copy($pathname, $hashfile)) {
1460                 throw new file_exception('storedfilecannotread', '', $pathname);
1461             }
1463             if (filesize($hashfile) !== $filesize) {
1464                 @unlink($hashfile);
1465                 throw new file_pool_content_exception($contenthash);
1466             }
1467             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1468         }
1471         return array($contenthash, $filesize, $newfile);
1472     }
1474     /**
1475      * Add string content to sha1 pool.
1476      *
1477      * @param string $content file content - binary string
1478      * @return array (contenthash, filesize, newfile)
1479      */
1480     public function add_string_to_pool($content) {
1481         $contenthash = sha1($content);
1482         $filesize = strlen($content); // binary length
1484         $hashpath = $this->path_from_hash($contenthash);
1485         $hashfile = "$hashpath/$contenthash";
1488         if (file_exists($hashfile)) {
1489             if (filesize($hashfile) !== $filesize) {
1490                 throw new file_pool_content_exception($contenthash);
1491             }
1492             $newfile = false;
1494         } else {
1495             if (!is_dir($hashpath)) {
1496                 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1497                     throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1498                 }
1499             }
1500             $newfile = true;
1502             file_put_contents($hashfile, $content);
1504             if (filesize($hashfile) !== $filesize) {
1505                 @unlink($hashfile);
1506                 throw new file_pool_content_exception($contenthash);
1507             }
1508             chmod($hashfile, $this->filepermissions); // fix permissions if needed
1509         }
1511         return array($contenthash, $filesize, $newfile);
1512     }
1514     /**
1515      * Serve file content using X-Sendfile header.
1516      * Please make sure that all headers are already sent
1517      * and the all access control checks passed.
1518      *
1519      * @param string $contenthash sah1 hash of the file content to be served
1520      * @return bool success
1521      */
1522     public function xsendfile($contenthash) {
1523         global $CFG;
1524         require_once("$CFG->libdir/xsendfilelib.php");
1526         $hashpath = $this->path_from_hash($contenthash);
1527         return xsendfile("$hashpath/$contenthash");
1528     }
1530     /**
1531      * Content exists
1532      *
1533      * @param string $contenthash
1534      * @return bool
1535      */
1536     public function content_exists($contenthash) {
1537         $dir = $this->path_from_hash($contenthash);
1538         $filepath = $dir . '/' . $contenthash;
1539         return file_exists($filepath);
1540     }
1542     /**
1543      * Return path to file with given hash.
1544      *
1545      * NOTE: must not be public, files in pool must not be modified
1546      *
1547      * @param string $contenthash content hash
1548      * @return string expected file location
1549      */
1550     protected function path_from_hash($contenthash) {
1551         $l1 = $contenthash[0].$contenthash[1];
1552         $l2 = $contenthash[2].$contenthash[3];
1553         return "$this->filedir/$l1/$l2";
1554     }
1556     /**
1557      * Return path to file with given hash.
1558      *
1559      * NOTE: must not be public, files in pool must not be modified
1560      *
1561      * @param string $contenthash content hash
1562      * @return string expected file location
1563      */
1564     protected function trash_path_from_hash($contenthash) {
1565         $l1 = $contenthash[0].$contenthash[1];
1566         $l2 = $contenthash[2].$contenthash[3];
1567         return "$this->trashdir/$l1/$l2";
1568     }
1570     /**
1571      * Tries to recover missing content of file from trash.
1572      *
1573      * @param stored_file $file stored_file instance
1574      * @return bool success
1575      */
1576     public function try_content_recovery($file) {
1577         $contenthash = $file->get_contenthash();
1578         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1579         if (!is_readable($trashfile)) {
1580             if (!is_readable($this->trashdir.'/'.$contenthash)) {
1581                 return false;
1582             }
1583             // nice, at least alternative trash file in trash root exists
1584             $trashfile = $this->trashdir.'/'.$contenthash;
1585         }
1586         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1587             //weird, better fail early
1588             return false;
1589         }
1590         $contentdir  = $this->path_from_hash($contenthash);
1591         $contentfile = $contentdir.'/'.$contenthash;
1592         if (file_exists($contentfile)) {
1593             //strange, no need to recover anything
1594             return true;
1595         }
1596         if (!is_dir($contentdir)) {
1597             if (!mkdir($contentdir, $this->dirpermissions, true)) {
1598                 return false;
1599             }
1600         }
1601         return rename($trashfile, $contentfile);
1602     }
1604     /**
1605      * Marks pool file as candidate for deleting.
1606      *
1607      * DO NOT call directly - reserved for core!!
1608      *
1609      * @param string $contenthash
1610      */
1611     public function deleted_file_cleanup($contenthash) {
1612         global $DB;
1614         //Note: this section is critical - in theory file could be reused at the same
1615         //      time, if this happens we can still recover the file from trash
1616         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1617             // file content is still used
1618             return;
1619         }
1620         //move content file to trash
1621         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1622         if (!file_exists($contentfile)) {
1623             //weird, but no problem
1624             return;
1625         }
1626         $trashpath = $this->trash_path_from_hash($contenthash);
1627         $trashfile = $trashpath.'/'.$contenthash;
1628         if (file_exists($trashfile)) {
1629             // we already have this content in trash, no need to move it there
1630             unlink($contentfile);
1631             return;
1632         }
1633         if (!is_dir($trashpath)) {
1634             mkdir($trashpath, $this->dirpermissions, true);
1635         }
1636         rename($contentfile, $trashfile);
1637         chmod($trashfile, $this->filepermissions); // fix permissions if needed
1638     }
1640     /**
1641      * When user referring to a moodle file, we build the reference field
1642      *
1643      * @param array $params
1644      * @return string
1645      */
1646     public static function pack_reference($params) {
1647         $params = (array)$params;
1648         $reference = array();
1649         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1650         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1651         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
1652         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
1653         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);;
1654         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
1655         return base64_encode(serialize($reference));
1656     }
1658     /**
1659      * Unpack reference field
1660      *
1661      * @param string $str
1662      * @return array
1663      */
1664     public static function unpack_reference($str) {
1665         return unserialize(base64_decode($str));
1666     }
1668     /**
1669      * Search references by providing reference content
1670      *
1671      * @param string $str
1672      * @return array
1673      */
1674     public function search_references($str) {
1675         global $DB;
1676         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1677                   FROM {files} f
1678              LEFT JOIN {files_reference} r
1679                        ON f.referencefileid = r.id
1680                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1681                  AND (f.component <> ? OR f.filearea <> ?)";
1683         $rs = $DB->get_recordset_sql($sql, array($str, 'user', 'draft'));
1684         $files = array();
1685         foreach ($rs as $filerecord) {
1686             $file = $this->get_file_instance($filerecord);
1687             if ($file->is_external_file()) {
1688                 $files[$filerecord->pathnamehash] = $file;
1689             }
1690         }
1692         return $files;
1693     }
1695     /**
1696      * Search references count by providing reference content
1697      *
1698      * @param string $str
1699      * @return int
1700      */
1701     public function search_references_count($str) {
1702         global $DB;
1703         $sql = "SELECT COUNT(f.id)
1704                   FROM {files} f
1705              LEFT JOIN {files_reference} r
1706                        ON f.referencefileid = r.id
1707                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1708                  AND (f.component <> ? OR f.filearea <> ?)";
1710         $count = $DB->count_records_sql($sql, array($str, 'user', 'draft'));
1711         return $count;
1712     }
1714     /**
1715      * Return all files referring to provided stored_file instance
1716      * This won't work for draft files
1717      *
1718      * @param stored_file $storedfile
1719      * @return array
1720      */
1721     public function get_references_by_storedfile($storedfile) {
1722         global $DB;
1724         $params = array();
1725         $params['contextid'] = $storedfile->get_contextid();
1726         $params['component'] = $storedfile->get_component();
1727         $params['filearea']  = $storedfile->get_filearea();
1728         $params['itemid']    = $storedfile->get_itemid();
1729         $params['filename']  = $storedfile->get_filename();
1730         $params['filepath']  = $storedfile->get_filepath();
1731         $params['userid']    = $storedfile->get_userid();
1733         $reference = self::pack_reference($params);
1735         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1736                   FROM {files} f
1737              LEFT JOIN {files_reference} r
1738                        ON f.referencefileid = r.id
1739                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1740                    AND (f.component <> ? OR f.filearea <> ?)";
1742         $rs = $DB->get_recordset_sql($sql, array($reference, 'user', 'draft'));
1743         $files = array();
1744         foreach ($rs as $filerecord) {
1745             $file = $this->get_file_instance($filerecord);
1746             if ($file->is_external_file()) {
1747                 $files[$filerecord->pathnamehash] = $file;
1748             }
1749         }
1751         return $files;
1752     }
1754     /**
1755      * Return the count files referring to provided stored_file instance
1756      * This won't work for draft files
1757      *
1758      * @param stored_file $storedfile
1759      * @return int
1760      */
1761     public function get_references_count_by_storedfile($storedfile) {
1762         global $DB;
1764         $params = array();
1765         $params['contextid'] = $storedfile->get_contextid();
1766         $params['component'] = $storedfile->get_component();
1767         $params['filearea']  = $storedfile->get_filearea();
1768         $params['itemid']    = $storedfile->get_itemid();
1769         $params['filename']  = $storedfile->get_filename();
1770         $params['filepath']  = $storedfile->get_filepath();
1771         $params['userid']    = $storedfile->get_userid();
1773         $reference = self::pack_reference($params);
1775         $sql = "SELECT COUNT(f.id)
1776                   FROM {files} f
1777              LEFT JOIN {files_reference} r
1778                        ON f.referencefileid = r.id
1779                  WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
1780                  AND (f.component <> ? OR f.filearea <> ?)";
1782         $count = $DB->count_records_sql($sql, array($reference, 'user', 'draft'));
1783         return $count;
1784     }
1786     /**
1787      * Convert file alias to local file
1788      *
1789      * @param stored_file $storedfile a stored_file instances
1790      * @return stored_file stored_file
1791      */
1792     public function import_external_file($storedfile) {
1793         global $CFG;
1794         require_once($CFG->dirroot.'/repository/lib.php');
1795         // sync external file
1796         repository::sync_external_file($storedfile);
1797         // Remove file references
1798         $storedfile->delete_reference();
1799         return $storedfile;
1800     }
1802     /**
1803      * Return mimetype by given file pathname
1804      *
1805      * If file has a known extension, we return the mimetype based on extension.
1806      * Otherwise (when possible) we try to get the mimetype from file contents.
1807      *
1808      * @param string $pathname
1809      * @return string
1810      */
1811     public static function mimetype($pathname) {
1812         $type = mimeinfo('type', $pathname);
1813         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
1814             $finfo = new finfo(FILEINFO_MIME_TYPE);
1815             $type = mimeinfo_from_type('type', $finfo->file($pathname));
1816         }
1817         return $type;
1818     }
1820     /**
1821      * Cron cleanup job.
1822      */
1823     public function cron() {
1824         global $CFG, $DB;
1826         // find out all stale draft areas (older than 4 days) and purge them
1827         // those are identified by time stamp of the /. root dir
1828         mtrace('Deleting old draft files... ', '');
1829         $old = time() - 60*60*24*4;
1830         $sql = "SELECT *
1831                   FROM {files}
1832                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1833                        AND timecreated < :old";
1834         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1835         foreach ($rs as $dir) {
1836             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1837         }
1838         $rs->close();
1839         mtrace('done.');
1841         // remove orphaned preview files (that is files in the core preview filearea without
1842         // the existing original file)
1843         mtrace('Deleting orphaned preview files... ', '');
1844         $sql = "SELECT p.*
1845                   FROM {files} p
1846              LEFT JOIN {files} o ON (p.filename = o.contenthash)
1847                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
1848                        AND o.id IS NULL";
1849         $syscontext = context_system::instance();
1850         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
1851         foreach ($rs as $orphan) {
1852             $file = $this->get_file_instance($orphan);
1853             if (!$file->is_directory()) {
1854                 $file->delete();
1855             }
1856         }
1857         $rs->close();
1858         mtrace('done.');
1860         // remove trash pool files once a day
1861         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1862         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1863             require_once($CFG->libdir.'/filelib.php');
1864             // Delete files that are associated with a context that no longer exists.
1865             mtrace('Cleaning up files from deleted contexts... ', '');
1866             $sql = "SELECT DISTINCT f.contextid
1867                     FROM {files} f
1868                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
1869                     WHERE c.id IS NULL";
1870             $rs = $DB->get_recordset_sql($sql);
1871             if ($rs->valid()) {
1872                 $fs = get_file_storage();
1873                 foreach ($rs as $ctx) {
1874                     $fs->delete_area_files($ctx->contextid);
1875                 }
1876             }
1877             $rs->close();
1878             mtrace('done.');
1880             mtrace('Deleting trash files... ', '');
1881             fulldelete($this->trashdir);
1882             set_config('fileslastcleanup', time());
1883             mtrace('done.');
1884         }
1885     }
1887     /**
1888      * Get the sql formated fields for a file instance to be created from a
1889      * {files} and {files_refernece} join.
1890      *
1891      * @param string $filesprefix the table prefix for the {files} table
1892      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
1893      * @return string the sql to go after a SELECT
1894      */
1895     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
1896         // Note, these fieldnames MUST NOT overlap between the two tables,
1897         // else problems like MDL-33172 occur.
1898         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
1899             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
1900             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid',
1901             'referencelastsync', 'referencelifetime');
1903         $referencefields = array('repositoryid', 'reference');
1905         // id is specifically named to prevent overlaping between the two tables.
1906         $fields = array();
1907         $fields[] = $filesprefix.'.id AS id';
1908         foreach ($filefields as $field) {
1909             $fields[] = "{$filesprefix}.{$field}";
1910         }
1912         foreach ($referencefields as $field) {
1913             $fields[] = "{$filesreferenceprefix}.{$field}";
1914         }
1916         return implode(', ', $fields);
1917     }