c481ebe8b5bfb6f860a2d4c315f062b8d4c72ca7
[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;
56     /** @var array List of formats supported by unoconv */
57     private $unoconvformats;
59     // Unoconv constants.
60     /** No errors */
61     const UNOCONVPATH_OK = 'ok';
62     /** Not set */
63     const UNOCONVPATH_EMPTY = 'empty';
64     /** Does not exist */
65     const UNOCONVPATH_DOESNOTEXIST = 'doesnotexist';
66     /** Is a dir */
67     const UNOCONVPATH_ISDIR = 'isdir';
68     /** Not executable */
69     const UNOCONVPATH_NOTEXECUTABLE = 'notexecutable';
70     /** Test file missing */
71     const UNOCONVPATH_NOTESTFILE = 'notestfile';
72     /** Version not supported */
73     const UNOCONVPATH_VERSIONNOTSUPPORTED = 'versionnotsupported';
74     /** Any other error */
75     const UNOCONVPATH_ERROR = 'error';
78     /**
79      * Constructor - do not use directly use {@link get_file_storage()} call instead.
80      *
81      * @param string $filedir full path to pool directory
82      * @param string $trashdir temporary storage of deleted area
83      * @param string $tempdir temporary storage of various files
84      * @param int $dirpermissions new directory permissions
85      * @param int $filepermissions new file permissions
86      */
87     public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
88         global $CFG;
90         $this->filedir         = $filedir;
91         $this->trashdir        = $trashdir;
92         $this->tempdir         = $tempdir;
93         $this->dirpermissions  = $dirpermissions;
94         $this->filepermissions = $filepermissions;
96         // make sure the file pool directory exists
97         if (!is_dir($this->filedir)) {
98             if (!mkdir($this->filedir, $this->dirpermissions, true)) {
99                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
100             }
101             // place warning file in file pool root
102             if (!file_exists($this->filedir.'/warning.txt')) {
103                 file_put_contents($this->filedir.'/warning.txt',
104                                   '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.');
105                 chmod($this->filedir.'/warning.txt', $CFG->filepermissions);
106             }
107         }
108         // make sure the file pool directory exists
109         if (!is_dir($this->trashdir)) {
110             if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
111                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
112             }
113         }
114     }
116     /**
117      * Calculates sha1 hash of unique full path name information.
118      *
119      * This hash is a unique file identifier - it is used to improve
120      * performance and overcome db index size limits.
121      *
122      * @param int $contextid context ID
123      * @param string $component component
124      * @param string $filearea file area
125      * @param int $itemid item ID
126      * @param string $filepath file path
127      * @param string $filename file name
128      * @return string sha1 hash
129      */
130     public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
131         return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
132     }
134     /**
135      * Does this file exist?
136      *
137      * @param int $contextid context ID
138      * @param string $component component
139      * @param string $filearea file area
140      * @param int $itemid item ID
141      * @param string $filepath file path
142      * @param string $filename file name
143      * @return bool
144      */
145     public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
146         $filepath = clean_param($filepath, PARAM_PATH);
147         $filename = clean_param($filename, PARAM_FILE);
149         if ($filename === '') {
150             $filename = '.';
151         }
153         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
154         return $this->file_exists_by_hash($pathnamehash);
155     }
157     /**
158      * Whether or not the file exist
159      *
160      * @param string $pathnamehash path name hash
161      * @return bool
162      */
163     public function file_exists_by_hash($pathnamehash) {
164         global $DB;
166         return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
167     }
169     /**
170      * Create instance of file class from database record.
171      *
172      * @param stdClass $filerecord record from the files table left join files_reference table
173      * @return stored_file instance of file abstraction class
174      */
175     public function get_file_instance(stdClass $filerecord) {
176         $storedfile = new stored_file($this, $filerecord, $this->filedir);
177         return $storedfile;
178     }
180     /**
181      * Get converted document.
182      *
183      * Get an alternate version of the specified document, if it is possible to convert.
184      *
185      * @param stored_file $file the file we want to preview
186      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
187      * @param boolean $forcerefresh If true, the file will be converted every time (not cached).
188      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
189      */
190     public function get_converted_document(stored_file $file, $format, $forcerefresh = false) {
192         $context = context_system::instance();
193         $path = '/' . $format . '/';
194         $conversion = $this->get_file($context->id, 'core', 'documentconversion', 0, $path, $file->get_contenthash());
196         if (!$conversion || $forcerefresh) {
197             $conversion = $this->create_converted_document($file, $format, $forcerefresh);
198             if (!$conversion) {
199                 return false;
200             }
201         }
203         return $conversion;
204     }
206     /**
207      * Verify the format is supported.
208      *
209      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
210      * @return bool - True if the format is supported for input.
211      */
212     protected function is_format_supported_by_unoconv($format) {
213         global $CFG;
215         if (!isset($this->unoconvformats)) {
216             // Ask unoconv for it's list of supported document formats.
217             $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' --show';
218             $pipes = array();
219             $pipesspec = array(2 => array('pipe', 'w'));
220             $proc = proc_open($cmd, $pipesspec, $pipes);
221             $programoutput = stream_get_contents($pipes[2]);
222             fclose($pipes[2]);
223             proc_close($proc);
224             $matches = array();
225             preg_match_all('/\[\.(.*)\]/', $programoutput, $matches);
227             $this->unoconvformats = $matches[1];
228             $this->unoconvformats = array_unique($this->unoconvformats);
229         }
231         $sanitized = trim(core_text::strtolower($format));
232         return in_array($sanitized, $this->unoconvformats);
233     }
235     /**
236      * Check if the installed version of unoconv is supported.
237      *
238      * @return bool true if the present version is supported, false otherwise.
239      */
240     public static function can_convert_documents() {
241         global $CFG;
242         $currentversion = 0;
243         $supportedversion = 0.7;
244         $unoconvbin = \escapeshellarg($CFG->pathtounoconv);
245         $command = "$unoconvbin --version";
246         exec($command, $output);
247         // If the command execution returned some output, then get the unoconv version.
248         if ($output) {
249             foreach ($output as $response) {
250                 if (preg_match('/unoconv (\\d+\\.\\d+)/', $response, $matches)) {
251                     $currentversion = (float)$matches[1];
252                 }
253             }
254             if ($currentversion < $supportedversion) {
255                 return false;
256             }
257             return true;
258         }
259         return false;
260     }
262     /**
263      * Regenerate the test pdf and send it direct to the browser.
264      */
265     public static function send_test_pdf() {
266         global $CFG;
267         require_once($CFG->libdir . '/filelib.php');
269         $filerecord = array(
270             'contextid' => \context_system::instance()->id,
271             'component' => 'test',
272             'filearea' => 'assignfeedback_editpdf',
273             'itemid' => 0,
274             'filepath' => '/',
275             'filename' => 'unoconv_test.docx'
276         );
278         // Get the fixture doc file content and generate and stored_file object.
279         $fs = get_file_storage();
280         $fixturefile = $CFG->libdir . '/tests/fixtures/unoconv-source.docx';
281         $fixturedata = file_get_contents($fixturefile);
282         $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'],
283                 $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);
284         if (!$testdocx) {
285             $testdocx = $fs->create_file_from_string($filerecord, $fixturedata);
287         }
289         // Convert the doc file to pdf and send it direct to the browser.
290         $result = $fs->get_converted_document($testdocx, 'pdf', true);
291         readfile_accel($result, 'application/pdf', true);
292     }
294     /**
295      * Check if unoconv configured path is correct and working.
296      *
297      * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
298      */
299     public static function test_unoconv_path() {
300         global $CFG;
301         $unoconvpath = $CFG->pathtounoconv;
303         $ret = new \stdClass();
304         $ret->status = self::UNOCONVPATH_OK;
305         $ret->message = null;
307         if (empty($unoconvpath)) {
308             $ret->status = self::UNOCONVPATH_EMPTY;
309             return $ret;
310         }
311         if (!file_exists($unoconvpath)) {
312             $ret->status = self::UNOCONVPATH_DOESNOTEXIST;
313             return $ret;
314         }
315         if (is_dir($unoconvpath)) {
316             $ret->status = self::UNOCONVPATH_ISDIR;
317             return $ret;
318         }
319         if (!file_is_executable($unoconvpath)) {
320             $ret->status = self::UNOCONVPATH_NOTEXECUTABLE;
321             return $ret;
322         }
323         if (!\file_storage::can_convert_documents()) {
324             $ret->status = self::UNOCONVPATH_VERSIONNOTSUPPORTED;
325             return $ret;
326         }
328         return $ret;
329     }
331     /**
332      * Perform a file format conversion on the specified document.
333      *
334      * @param stored_file $file the file we want to preview
335      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
336      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
337      */
338     protected function create_converted_document(stored_file $file, $format, $forcerefresh = false) {
339         global $CFG;
341         if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
342             // No conversions are possible, sorry.
343             return false;
344         }
346         $fileextension = core_text::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
347         if (!self::is_format_supported_by_unoconv($fileextension)) {
348             return false;
349         }
351         if (!self::is_format_supported_by_unoconv($format)) {
352             return false;
353         }
355         // Copy the file to the tmp dir.
356         $uniqdir = "core_file/conversions/" . uniqid($file->get_id() . "-", true);
357         $tmp = make_temp_directory($uniqdir);
358         $ext = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
359         // Safety.
360         $localfilename = $file->get_id() . '.' . $ext;
362         $filename = $tmp . '/' . $localfilename;
363         try {
364             // This function can either return false, or throw an exception so we need to handle both.
365             if ($file->copy_content_to($filename) === false) {
366                 throw new file_exception('storedfileproblem', 'Could not copy file contents to temp file.');
367             }
368         } catch (file_exception $fe) {
369             remove_dir($tmp);
370             throw $fe;
371         }
373         $newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format;
375         // Safety.
376         $newtmpfile = $tmp . '/' . clean_param($newtmpfile, PARAM_FILE);
378         $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' .
379                escapeshellarg('-f') . ' ' .
380                escapeshellarg($format) . ' ' .
381                escapeshellarg('-o') . ' ' .
382                escapeshellarg($newtmpfile) . ' ' .
383                escapeshellarg($filename);
385         $output = null;
386         $currentdir = getcwd();
387         chdir($tmp);
388         $result = exec($cmd, $output);
389         chdir($currentdir);
390         touch($newtmpfile);
391         if (filesize($newtmpfile) === 0) {
392             remove_dir($tmp);
393             // Cleanup.
394             return false;
395         }
397         $context = context_system::instance();
398         $path = '/' . $format . '/';
399         $record = array(
400             'contextid' => $context->id,
401             'component' => 'core',
402             'filearea'  => 'documentconversion',
403             'itemid'    => 0,
404             'filepath'  => $path,
405             'filename'  => $file->get_contenthash(),
406         );
408         if ($forcerefresh) {
409             $existing = $this->get_file($context->id, 'core', 'documentconversion', 0, $path, $file->get_contenthash());
410             if ($existing) {
411                 $existing->delete();
412             }
413         }
415         $convertedfile = $this->create_file_from_pathname($record, $newtmpfile);
416         // Cleanup.
417         remove_dir($tmp);
418         return $convertedfile;
419     }
421     /**
422      * Returns an image file that represent the given stored file as a preview
423      *
424      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
425      * future, the support for other mimetypes can be added, too (eg. generate an image
426      * preview of PDF, text documents etc).
427      *
428      * @param stored_file $file the file we want to preview
429      * @param string $mode preview mode, eg. 'thumb'
430      * @return stored_file|bool false if unable to create the preview, stored file otherwise
431      */
432     public function get_file_preview(stored_file $file, $mode) {
434         $context = context_system::instance();
435         $path = '/' . trim($mode, '/') . '/';
436         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
438         if (!$preview) {
439             $preview = $this->create_file_preview($file, $mode);
440             if (!$preview) {
441                 return false;
442             }
443         }
445         return $preview;
446     }
448     /**
449      * Return an available file name.
450      *
451      * This will return the next available file name in the area, adding/incrementing a suffix
452      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
453      *
454      * If the file name passed is available without modification, it is returned as is.
455      *
456      * @param int $contextid context ID.
457      * @param string $component component.
458      * @param string $filearea file area.
459      * @param int $itemid area item ID.
460      * @param string $filepath the file path.
461      * @param string $filename the file name.
462      * @return string available file name.
463      * @throws coding_exception if the file name is invalid.
464      * @since Moodle 2.5
465      */
466     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
467         global $DB;
469         // Do not accept '.' or an empty file name (zero is acceptable).
470         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
471             throw new coding_exception('Invalid file name passed', $filename);
472         }
474         // The file does not exist, we return the same file name.
475         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
476             return $filename;
477         }
479         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
480         $pathinfo = pathinfo($filename);
481         $basename = $pathinfo['filename'];
482         $matches = array();
483         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
484             $basename = $matches[1];
485         }
487         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
488         if (isset($pathinfo['extension'])) {
489             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
490         }
492         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
493         $filenamelen = $DB->sql_length('f.filename');
494         $sql = "SELECT filename
495                 FROM {files} f
496                 WHERE
497                     f.contextid = :contextid AND
498                     f.component = :component AND
499                     f.filearea = :filearea AND
500                     f.itemid = :itemid AND
501                     f.filepath = :filepath AND
502                     $filenamelikesql
503                 ORDER BY
504                     $filenamelen DESC,
505                     f.filename DESC";
506         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
507                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
508         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
510         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
511         // would both be returned, but only the one only containing digits should be used.
512         $number = 1;
513         foreach ($results as $result) {
514             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
515             $matches = array();
516             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
517                 $number = $matches[2] + 1;
518                 break;
519             }
520         }
522         // Constructing the new filename.
523         $newfilename = $basename . ' (' . $number . ')';
524         if (isset($pathinfo['extension'])) {
525             $newfilename .= '.' . $pathinfo['extension'];
526         }
528         return $newfilename;
529     }
531     /**
532      * Return an available directory name.
533      *
534      * This will return the next available directory name in the area, adding/incrementing a suffix
535      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
536      *
537      * If the file path passed is available without modification, it is returned as is.
538      *
539      * @param int $contextid context ID.
540      * @param string $component component.
541      * @param string $filearea file area.
542      * @param int $itemid area item ID.
543      * @param string $suggestedpath the suggested file path.
544      * @return string available file path
545      * @since Moodle 2.5
546      */
547     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
548         global $DB;
550         // Ensure suggestedpath has trailing '/'
551         $suggestedpath = rtrim($suggestedpath, '/'). '/';
553         // The directory does not exist, we return the same file path.
554         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
555             return $suggestedpath;
556         }
558         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
559         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
560             $suggestedpath = $matches[1]. '/';
561         }
563         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
565         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
566         $filepathlen = $DB->sql_length('f.filepath');
567         $sql = "SELECT filepath
568                 FROM {files} f
569                 WHERE
570                     f.contextid = :contextid AND
571                     f.component = :component AND
572                     f.filearea = :filearea AND
573                     f.itemid = :itemid AND
574                     f.filename = :filename AND
575                     $filepathlikesql
576                 ORDER BY
577                     $filepathlen DESC,
578                     f.filepath DESC";
579         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
580                 'filename' => '.', 'filepathlike' => $filepathlike);
581         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
583         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
584         // would both be returned, but only the one only containing digits should be used.
585         $number = 1;
586         foreach ($results as $result) {
587             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
588                 $number = (int)($matches[1]) + 1;
589                 break;
590             }
591         }
593         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
594     }
596     /**
597      * Generates a preview image for the stored file
598      *
599      * @param stored_file $file the file we want to preview
600      * @param string $mode preview mode, eg. 'thumb'
601      * @return stored_file|bool the newly created preview file or false
602      */
603     protected function create_file_preview(stored_file $file, $mode) {
605         $mimetype = $file->get_mimetype();
607         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
608             // make a preview of the image
609             $data = $this->create_imagefile_preview($file, $mode);
611         } else {
612             // unable to create the preview of this mimetype yet
613             return false;
614         }
616         if (empty($data)) {
617             return false;
618         }
620         $context = context_system::instance();
621         $record = array(
622             'contextid' => $context->id,
623             'component' => 'core',
624             'filearea'  => 'preview',
625             'itemid'    => 0,
626             'filepath'  => '/' . trim($mode, '/') . '/',
627             'filename'  => $file->get_contenthash(),
628         );
630         $imageinfo = getimagesizefromstring($data);
631         if ($imageinfo) {
632             $record['mimetype'] = $imageinfo['mime'];
633         }
635         return $this->create_file_from_string($record, $data);
636     }
638     /**
639      * Generates a preview for the stored image file
640      *
641      * @param stored_file $file the image we want to preview
642      * @param string $mode preview mode, eg. 'thumb'
643      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
644      */
645     protected function create_imagefile_preview(stored_file $file, $mode) {
646         global $CFG;
647         require_once($CFG->libdir.'/gdlib.php');
649         if ($mode === 'tinyicon') {
650             $data = $file->generate_image_thumbnail(24, 24);
652         } else if ($mode === 'thumb') {
653             $data = $file->generate_image_thumbnail(90, 90);
655         } else if ($mode === 'bigthumb') {
656             $data = $file->generate_image_thumbnail(250, 250);
658         } else {
659             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
660         }
662         return $data;
663     }
665     /**
666      * Fetch file using local file id.
667      *
668      * Please do not rely on file ids, it is usually easier to use
669      * pathname hashes instead.
670      *
671      * @param int $fileid file ID
672      * @return stored_file|bool stored_file instance if exists, false if not
673      */
674     public function get_file_by_id($fileid) {
675         global $DB;
677         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
678                   FROM {files} f
679              LEFT JOIN {files_reference} r
680                        ON f.referencefileid = r.id
681                  WHERE f.id = ?";
682         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
683             return $this->get_file_instance($filerecord);
684         } else {
685             return false;
686         }
687     }
689     /**
690      * Fetch file using local file full pathname hash
691      *
692      * @param string $pathnamehash path name hash
693      * @return stored_file|bool stored_file instance if exists, false if not
694      */
695     public function get_file_by_hash($pathnamehash) {
696         global $DB;
698         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
699                   FROM {files} f
700              LEFT JOIN {files_reference} r
701                        ON f.referencefileid = r.id
702                  WHERE f.pathnamehash = ?";
703         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
704             return $this->get_file_instance($filerecord);
705         } else {
706             return false;
707         }
708     }
710     /**
711      * Fetch locally stored file.
712      *
713      * @param int $contextid context ID
714      * @param string $component component
715      * @param string $filearea file area
716      * @param int $itemid item ID
717      * @param string $filepath file path
718      * @param string $filename file name
719      * @return stored_file|bool stored_file instance if exists, false if not
720      */
721     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
722         $filepath = clean_param($filepath, PARAM_PATH);
723         $filename = clean_param($filename, PARAM_FILE);
725         if ($filename === '') {
726             $filename = '.';
727         }
729         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
730         return $this->get_file_by_hash($pathnamehash);
731     }
733     /**
734      * Are there any files (or directories)
735      *
736      * @param int $contextid context ID
737      * @param string $component component
738      * @param string $filearea file area
739      * @param bool|int $itemid item id or false if all items
740      * @param bool $ignoredirs whether or not ignore directories
741      * @return bool empty
742      */
743     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
744         global $DB;
746         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
747         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
749         if ($itemid !== false) {
750             $params['itemid'] = $itemid;
751             $where .= " AND itemid = :itemid";
752         }
754         if ($ignoredirs) {
755             $sql = "SELECT 'x'
756                       FROM {files}
757                      WHERE $where AND filename <> '.'";
758         } else {
759             $sql = "SELECT 'x'
760                       FROM {files}
761                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
762         }
764         return !$DB->record_exists_sql($sql, $params);
765     }
767     /**
768      * Returns all files belonging to given repository
769      *
770      * @param int $repositoryid
771      * @param string $sort A fragment of SQL to use for sorting
772      */
773     public function get_external_files($repositoryid, $sort = '') {
774         global $DB;
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 r.repositoryid = ?";
780         if (!empty($sort)) {
781             $sql .= " ORDER BY {$sort}";
782         }
784         $result = array();
785         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
786         foreach ($filerecords as $filerecord) {
787             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
788         }
789         return $result;
790     }
792     /**
793      * Returns all area files (optionally limited by itemid)
794      *
795      * @param int $contextid context ID
796      * @param string $component component
797      * @param mixed $filearea file area/s, you cannot specify multiple fileareas as well as an itemid
798      * @param int $itemid item ID or all files if not specified
799      * @param string $sort A fragment of SQL to use for sorting
800      * @param bool $includedirs whether or not include directories
801      * @param int $updatedsince return files updated since this time
802      * @param int $limitfrom return a subset of records, starting at this point (optional).
803      * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
804      * @return stored_file[] array of stored_files indexed by pathanmehash
805      */
806     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename",
807             $includedirs = true, $updatedsince = 0, $limitfrom = 0, $limitnum = 0) {
808         global $DB;
810         list($areasql, $conditions) = $DB->get_in_or_equal($filearea, SQL_PARAMS_NAMED);
811         $conditions['contextid'] = $contextid;
812         $conditions['component'] = $component;
814         if ($itemid !== false && is_array($filearea)) {
815             throw new coding_exception('You cannot specify multiple fileareas as well as an itemid.');
816         } else if ($itemid !== false) {
817             $itemidsql = ' AND f.itemid = :itemid ';
818             $conditions['itemid'] = $itemid;
819         } else {
820             $itemidsql = '';
821         }
823         $updatedsincesql = '';
824         if (!empty($updatedsince)) {
825             $conditions['time'] = $updatedsince;
826             $updatedsincesql = 'AND f.timemodified > :time';
827         }
829         $includedirssql = '';
830         if (!$includedirs) {
831             $includedirssql = 'AND f.filename != :dot';
832             $conditions['dot'] = '.';
833         }
835         if ($limitfrom && !$limitnum) {
836             throw new coding_exception('If specifying $limitfrom you must also specify $limitnum');
837         }
839         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
840                   FROM {files} f
841              LEFT JOIN {files_reference} r
842                        ON f.referencefileid = r.id
843                  WHERE f.contextid = :contextid
844                        AND f.component = :component
845                        AND f.filearea $areasql
846                        $includedirssql
847                        $updatedsincesql
848                        $itemidsql";
849         if (!empty($sort)) {
850             $sql .= " ORDER BY {$sort}";
851         }
853         $result = array();
854         $filerecords = $DB->get_records_sql($sql, $conditions, $limitfrom, $limitnum);
855         foreach ($filerecords as $filerecord) {
856             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
857         }
858         return $result;
859     }
861     /**
862      * Returns array based tree structure of area files
863      *
864      * @param int $contextid context ID
865      * @param string $component component
866      * @param string $filearea file area
867      * @param int $itemid item ID
868      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
869      */
870     public function get_area_tree($contextid, $component, $filearea, $itemid) {
871         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
872         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
873         // first create directory structure
874         foreach ($files as $hash=>$dir) {
875             if (!$dir->is_directory()) {
876                 continue;
877             }
878             unset($files[$hash]);
879             if ($dir->get_filepath() === '/') {
880                 $result['dirfile'] = $dir;
881                 continue;
882             }
883             $parts = explode('/', trim($dir->get_filepath(),'/'));
884             $pointer =& $result;
885             foreach ($parts as $part) {
886                 if ($part === '') {
887                     continue;
888                 }
889                 if (!isset($pointer['subdirs'][$part])) {
890                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
891                 }
892                 $pointer =& $pointer['subdirs'][$part];
893             }
894             $pointer['dirfile'] = $dir;
895             unset($pointer);
896         }
897         foreach ($files as $hash=>$file) {
898             $parts = explode('/', trim($file->get_filepath(),'/'));
899             $pointer =& $result;
900             foreach ($parts as $part) {
901                 if ($part === '') {
902                     continue;
903                 }
904                 $pointer =& $pointer['subdirs'][$part];
905             }
906             $pointer['files'][$file->get_filename()] = $file;
907             unset($pointer);
908         }
909         $result = $this->sort_area_tree($result);
910         return $result;
911     }
913     /**
914      * Sorts the result of {@link file_storage::get_area_tree()}.
915      *
916      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
917      * @return array of sorted results
918      */
919     protected function sort_area_tree($tree) {
920         foreach ($tree as $key => &$value) {
921             if ($key == 'subdirs') {
922                 core_collator::ksort($value, core_collator::SORT_NATURAL);
923                 foreach ($value as $subdirname => &$subtree) {
924                     $subtree = $this->sort_area_tree($subtree);
925                 }
926             } else if ($key == 'files') {
927                 core_collator::ksort($value, core_collator::SORT_NATURAL);
928             }
929         }
930         return $tree;
931     }
933     /**
934      * Returns all files and optionally directories
935      *
936      * @param int $contextid context ID
937      * @param string $component component
938      * @param string $filearea file area
939      * @param int $itemid item ID
940      * @param int $filepath directory path
941      * @param bool $recursive include all subdirectories
942      * @param bool $includedirs include files and directories
943      * @param string $sort A fragment of SQL to use for sorting
944      * @return array of stored_files indexed by pathanmehash
945      */
946     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
947         global $DB;
949         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
950             return array();
951         }
953         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
955         if ($recursive) {
957             $dirs = $includedirs ? "" : "AND filename <> '.'";
958             $length = core_text::strlen($filepath);
960             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
961                       FROM {files} f
962                  LEFT JOIN {files_reference} r
963                            ON f.referencefileid = r.id
964                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
965                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
966                            AND f.id <> :dirid
967                            $dirs
968                            $orderby";
969             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
971             $files = array();
972             $dirs  = array();
973             $filerecords = $DB->get_records_sql($sql, $params);
974             foreach ($filerecords as $filerecord) {
975                 if ($filerecord->filename == '.') {
976                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
977                 } else {
978                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
979                 }
980             }
981             $result = array_merge($dirs, $files);
983         } else {
984             $result = array();
985             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
987             $length = core_text::strlen($filepath);
989             if ($includedirs) {
990                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
991                           FROM {files} f
992                      LEFT JOIN {files_reference} r
993                                ON f.referencefileid = r.id
994                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
995                                AND f.itemid = :itemid AND f.filename = '.'
996                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
997                                AND f.id <> :dirid
998                                $orderby";
999                 $reqlevel = substr_count($filepath, '/') + 1;
1000                 $filerecords = $DB->get_records_sql($sql, $params);
1001                 foreach ($filerecords as $filerecord) {
1002                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
1003                         continue;
1004                     }
1005                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1006                 }
1007             }
1009             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1010                       FROM {files} f
1011                  LEFT JOIN {files_reference} r
1012                            ON f.referencefileid = r.id
1013                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
1014                            AND f.filepath = :filepath AND f.filename <> '.'
1015                            $orderby";
1017             $filerecords = $DB->get_records_sql($sql, $params);
1018             foreach ($filerecords as $filerecord) {
1019                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
1020             }
1021         }
1023         return $result;
1024     }
1026     /**
1027      * Delete all area files (optionally limited by itemid).
1028      *
1029      * @param int $contextid context ID
1030      * @param string $component component
1031      * @param string $filearea file area or all areas in context if not specified
1032      * @param int $itemid item ID or all files if not specified
1033      * @return bool success
1034      */
1035     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
1036         global $DB;
1038         $conditions = array('contextid'=>$contextid);
1039         if ($component !== false) {
1040             $conditions['component'] = $component;
1041         }
1042         if ($filearea !== false) {
1043             $conditions['filearea'] = $filearea;
1044         }
1045         if ($itemid !== false) {
1046             $conditions['itemid'] = $itemid;
1047         }
1049         $filerecords = $DB->get_records('files', $conditions);
1050         foreach ($filerecords as $filerecord) {
1051             $this->get_file_instance($filerecord)->delete();
1052         }
1054         return true; // BC only
1055     }
1057     /**
1058      * Delete all the files from certain areas where itemid is limited by an
1059      * arbitrary bit of SQL.
1060      *
1061      * @param int $contextid the id of the context the files belong to. Must be given.
1062      * @param string $component the owning component. Must be given.
1063      * @param string $filearea the file area name. Must be given.
1064      * @param string $itemidstest an SQL fragment that the itemid must match. Used
1065      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
1066      *      and may not used named parameters called contextid, component or filearea.
1067      * @param array $params any query params used by $itemidstest.
1068      */
1069     public function delete_area_files_select($contextid, $component,
1070             $filearea, $itemidstest, array $params = null) {
1071         global $DB;
1073         $where = "contextid = :contextid
1074                 AND component = :component
1075                 AND filearea = :filearea
1076                 AND itemid $itemidstest";
1077         $params['contextid'] = $contextid;
1078         $params['component'] = $component;
1079         $params['filearea'] = $filearea;
1081         $filerecords = $DB->get_recordset_select('files', $where, $params);
1082         foreach ($filerecords as $filerecord) {
1083             $this->get_file_instance($filerecord)->delete();
1084         }
1085         $filerecords->close();
1086     }
1088     /**
1089      * Delete all files associated with the given component.
1090      *
1091      * @param string $component the component owning the file
1092      */
1093     public function delete_component_files($component) {
1094         global $DB;
1096         $filerecords = $DB->get_recordset('files', array('component' => $component));
1097         foreach ($filerecords as $filerecord) {
1098             $this->get_file_instance($filerecord)->delete();
1099         }
1100         $filerecords->close();
1101     }
1103     /**
1104      * Move all the files in a file area from one context to another.
1105      *
1106      * @param int $oldcontextid the context the files are being moved from.
1107      * @param int $newcontextid the context the files are being moved to.
1108      * @param string $component the plugin that these files belong to.
1109      * @param string $filearea the name of the file area.
1110      * @param int $itemid file item ID
1111      * @return int the number of files moved, for information.
1112      */
1113     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
1114         // Note, this code is based on some code that Petr wrote in
1115         // forum_move_attachments in mod/forum/lib.php. I moved it here because
1116         // I needed it in the question code too.
1117         $count = 0;
1119         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
1120         foreach ($oldfiles as $oldfile) {
1121             $filerecord = new stdClass();
1122             $filerecord->contextid = $newcontextid;
1123             $this->create_file_from_storedfile($filerecord, $oldfile);
1124             $count += 1;
1125         }
1127         if ($count) {
1128             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
1129         }
1131         return $count;
1132     }
1134     /**
1135      * Recursively creates directory.
1136      *
1137      * @param int $contextid context ID
1138      * @param string $component component
1139      * @param string $filearea file area
1140      * @param int $itemid item ID
1141      * @param string $filepath file path
1142      * @param int $userid the user ID
1143      * @return bool success
1144      */
1145     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
1146         global $DB;
1148         // validate all parameters, we do not want any rubbish stored in database, right?
1149         if (!is_number($contextid) or $contextid < 1) {
1150             throw new file_exception('storedfileproblem', 'Invalid contextid');
1151         }
1153         $component = clean_param($component, PARAM_COMPONENT);
1154         if (empty($component)) {
1155             throw new file_exception('storedfileproblem', 'Invalid component');
1156         }
1158         $filearea = clean_param($filearea, PARAM_AREA);
1159         if (empty($filearea)) {
1160             throw new file_exception('storedfileproblem', 'Invalid filearea');
1161         }
1163         if (!is_number($itemid) or $itemid < 0) {
1164             throw new file_exception('storedfileproblem', 'Invalid itemid');
1165         }
1167         $filepath = clean_param($filepath, PARAM_PATH);
1168         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1169             // path must start and end with '/'
1170             throw new file_exception('storedfileproblem', 'Invalid file path');
1171         }
1173         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1175         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1176             return $dir_info;
1177         }
1179         static $contenthash = null;
1180         if (!$contenthash) {
1181             $this->add_string_to_pool('');
1182             $contenthash = sha1('');
1183         }
1185         $now = time();
1187         $dir_record = new stdClass();
1188         $dir_record->contextid = $contextid;
1189         $dir_record->component = $component;
1190         $dir_record->filearea  = $filearea;
1191         $dir_record->itemid    = $itemid;
1192         $dir_record->filepath  = $filepath;
1193         $dir_record->filename  = '.';
1194         $dir_record->contenthash  = $contenthash;
1195         $dir_record->filesize  = 0;
1197         $dir_record->timecreated  = $now;
1198         $dir_record->timemodified = $now;
1199         $dir_record->mimetype     = null;
1200         $dir_record->userid       = $userid;
1202         $dir_record->pathnamehash = $pathnamehash;
1204         $DB->insert_record('files', $dir_record);
1205         $dir_info = $this->get_file_by_hash($pathnamehash);
1207         if ($filepath !== '/') {
1208             //recurse to parent dirs
1209             $filepath = trim($filepath, '/');
1210             $filepath = explode('/', $filepath);
1211             array_pop($filepath);
1212             $filepath = implode('/', $filepath);
1213             $filepath = ($filepath === '') ? '/' : "/$filepath/";
1214             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1215         }
1217         return $dir_info;
1218     }
1220     /**
1221      * Add new local file based on existing local file.
1222      *
1223      * @param stdClass|array $filerecord object or array describing changes
1224      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1225      * @return stored_file instance of newly created file
1226      */
1227     public function create_file_from_storedfile($filerecord, $fileorid) {
1228         global $DB;
1230         if ($fileorid instanceof stored_file) {
1231             $fid = $fileorid->get_id();
1232         } else {
1233             $fid = $fileorid;
1234         }
1236         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1238         unset($filerecord['id']);
1239         unset($filerecord['filesize']);
1240         unset($filerecord['contenthash']);
1241         unset($filerecord['pathnamehash']);
1243         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1244                   FROM {files} f
1245              LEFT JOIN {files_reference} r
1246                        ON f.referencefileid = r.id
1247                  WHERE f.id = ?";
1249         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1250             throw new file_exception('storedfileproblem', 'File does not exist');
1251         }
1253         unset($newrecord->id);
1255         foreach ($filerecord as $key => $value) {
1256             // validate all parameters, we do not want any rubbish stored in database, right?
1257             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1258                 throw new file_exception('storedfileproblem', 'Invalid contextid');
1259             }
1261             if ($key == 'component') {
1262                 $value = clean_param($value, PARAM_COMPONENT);
1263                 if (empty($value)) {
1264                     throw new file_exception('storedfileproblem', 'Invalid component');
1265                 }
1266             }
1268             if ($key == 'filearea') {
1269                 $value = clean_param($value, PARAM_AREA);
1270                 if (empty($value)) {
1271                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1272                 }
1273             }
1275             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1276                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1277             }
1280             if ($key == 'filepath') {
1281                 $value = clean_param($value, PARAM_PATH);
1282                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1283                     // path must start and end with '/'
1284                     throw new file_exception('storedfileproblem', 'Invalid file path');
1285                 }
1286             }
1288             if ($key == 'filename') {
1289                 $value = clean_param($value, PARAM_FILE);
1290                 if ($value === '') {
1291                     // path must start and end with '/'
1292                     throw new file_exception('storedfileproblem', 'Invalid file name');
1293                 }
1294             }
1296             if ($key === 'timecreated' or $key === 'timemodified') {
1297                 if (!is_number($value)) {
1298                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1299                 }
1300                 if ($value < 0) {
1301                     //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)
1302                     $value = 0;
1303                 }
1304             }
1306             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1307                 $value = clean_param($value, PARAM_INT);
1308             }
1310             $newrecord->$key = $value;
1311         }
1313         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1315         if ($newrecord->filename === '.') {
1316             // special case - only this function supports directories ;-)
1317             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1318             // update the existing directory with the new data
1319             $newrecord->id = $directory->get_id();
1320             $DB->update_record('files', $newrecord);
1321             return $this->get_file_instance($newrecord);
1322         }
1324         // note: referencefileid is copied from the original file so that
1325         // creating a new file from an existing alias creates new alias implicitly.
1326         // here we just check the database consistency.
1327         if (!empty($newrecord->repositoryid)) {
1328             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1329                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1330             }
1331         }
1333         try {
1334             $newrecord->id = $DB->insert_record('files', $newrecord);
1335         } catch (dml_exception $e) {
1336             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1337                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1338         }
1341         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1343         return $this->get_file_instance($newrecord);
1344     }
1346     /**
1347      * Add new local file.
1348      *
1349      * @param stdClass|array $filerecord object or array describing file
1350      * @param string $url the URL to the file
1351      * @param array $options {@link download_file_content()} options
1352      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1353      * @return stored_file
1354      */
1355     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1357         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1358         $filerecord = (object)$filerecord; // We support arrays too.
1360         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1361         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1362         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1363         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1364         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1365         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1366         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1368         if (!isset($filerecord->filename)) {
1369             $parts = explode('/', $url);
1370             $filename = array_pop($parts);
1371             $filerecord->filename = clean_param($filename, PARAM_FILE);
1372         }
1373         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1374         $filerecord->source = clean_param($source, PARAM_URL);
1376         if ($usetempfile) {
1377             check_dir_exists($this->tempdir);
1378             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1379             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1380             if ($content === false) {
1381                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1382             }
1383             try {
1384                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1385                 @unlink($tmpfile);
1386                 return $newfile;
1387             } catch (Exception $e) {
1388                 @unlink($tmpfile);
1389                 throw $e;
1390             }
1392         } else {
1393             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1394             if ($content === false) {
1395                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1396             }
1397             return $this->create_file_from_string($filerecord, $content);
1398         }
1399     }
1401     /**
1402      * Add new local file.
1403      *
1404      * @param stdClass|array $filerecord object or array describing file
1405      * @param string $pathname path to file or content of file
1406      * @return stored_file
1407      */
1408     public function create_file_from_pathname($filerecord, $pathname) {
1409         global $DB;
1411         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1412         $filerecord = (object)$filerecord; // We support arrays too.
1414         // validate all parameters, we do not want any rubbish stored in database, right?
1415         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1416             throw new file_exception('storedfileproblem', 'Invalid contextid');
1417         }
1419         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1420         if (empty($filerecord->component)) {
1421             throw new file_exception('storedfileproblem', 'Invalid component');
1422         }
1424         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1425         if (empty($filerecord->filearea)) {
1426             throw new file_exception('storedfileproblem', 'Invalid filearea');
1427         }
1429         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1430             throw new file_exception('storedfileproblem', 'Invalid itemid');
1431         }
1433         if (!empty($filerecord->sortorder)) {
1434             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1435                 $filerecord->sortorder = 0;
1436             }
1437         } else {
1438             $filerecord->sortorder = 0;
1439         }
1441         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1442         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1443             // path must start and end with '/'
1444             throw new file_exception('storedfileproblem', 'Invalid file path');
1445         }
1447         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1448         if ($filerecord->filename === '') {
1449             // filename must not be empty
1450             throw new file_exception('storedfileproblem', 'Invalid file name');
1451         }
1453         $now = time();
1454         if (isset($filerecord->timecreated)) {
1455             if (!is_number($filerecord->timecreated)) {
1456                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1457             }
1458             if ($filerecord->timecreated < 0) {
1459                 //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)
1460                 $filerecord->timecreated = 0;
1461             }
1462         } else {
1463             $filerecord->timecreated = $now;
1464         }
1466         if (isset($filerecord->timemodified)) {
1467             if (!is_number($filerecord->timemodified)) {
1468                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1469             }
1470             if ($filerecord->timemodified < 0) {
1471                 //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)
1472                 $filerecord->timemodified = 0;
1473             }
1474         } else {
1475             $filerecord->timemodified = $now;
1476         }
1478         $newrecord = new stdClass();
1480         $newrecord->contextid = $filerecord->contextid;
1481         $newrecord->component = $filerecord->component;
1482         $newrecord->filearea  = $filerecord->filearea;
1483         $newrecord->itemid    = $filerecord->itemid;
1484         $newrecord->filepath  = $filerecord->filepath;
1485         $newrecord->filename  = $filerecord->filename;
1487         $newrecord->timecreated  = $filerecord->timecreated;
1488         $newrecord->timemodified = $filerecord->timemodified;
1489         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1490         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1491         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1492         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1493         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1494         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1495         $newrecord->sortorder    = $filerecord->sortorder;
1497         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1499         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1501         try {
1502             $newrecord->id = $DB->insert_record('files', $newrecord);
1503         } catch (dml_exception $e) {
1504             if ($newfile) {
1505                 $this->deleted_file_cleanup($newrecord->contenthash);
1506             }
1507             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1508                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1509         }
1511         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1513         return $this->get_file_instance($newrecord);
1514     }
1516     /**
1517      * Add new local file.
1518      *
1519      * @param stdClass|array $filerecord object or array describing file
1520      * @param string $content content of file
1521      * @return stored_file
1522      */
1523     public function create_file_from_string($filerecord, $content) {
1524         global $DB;
1526         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1527         $filerecord = (object)$filerecord; // We support arrays too.
1529         // validate all parameters, we do not want any rubbish stored in database, right?
1530         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1531             throw new file_exception('storedfileproblem', 'Invalid contextid');
1532         }
1534         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1535         if (empty($filerecord->component)) {
1536             throw new file_exception('storedfileproblem', 'Invalid component');
1537         }
1539         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1540         if (empty($filerecord->filearea)) {
1541             throw new file_exception('storedfileproblem', 'Invalid filearea');
1542         }
1544         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1545             throw new file_exception('storedfileproblem', 'Invalid itemid');
1546         }
1548         if (!empty($filerecord->sortorder)) {
1549             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1550                 $filerecord->sortorder = 0;
1551             }
1552         } else {
1553             $filerecord->sortorder = 0;
1554         }
1556         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1557         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1558             // path must start and end with '/'
1559             throw new file_exception('storedfileproblem', 'Invalid file path');
1560         }
1562         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1563         if ($filerecord->filename === '') {
1564             // path must start and end with '/'
1565             throw new file_exception('storedfileproblem', 'Invalid file name');
1566         }
1568         $now = time();
1569         if (isset($filerecord->timecreated)) {
1570             if (!is_number($filerecord->timecreated)) {
1571                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1572             }
1573             if ($filerecord->timecreated < 0) {
1574                 //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)
1575                 $filerecord->timecreated = 0;
1576             }
1577         } else {
1578             $filerecord->timecreated = $now;
1579         }
1581         if (isset($filerecord->timemodified)) {
1582             if (!is_number($filerecord->timemodified)) {
1583                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1584             }
1585             if ($filerecord->timemodified < 0) {
1586                 //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)
1587                 $filerecord->timemodified = 0;
1588             }
1589         } else {
1590             $filerecord->timemodified = $now;
1591         }
1593         $newrecord = new stdClass();
1595         $newrecord->contextid = $filerecord->contextid;
1596         $newrecord->component = $filerecord->component;
1597         $newrecord->filearea  = $filerecord->filearea;
1598         $newrecord->itemid    = $filerecord->itemid;
1599         $newrecord->filepath  = $filerecord->filepath;
1600         $newrecord->filename  = $filerecord->filename;
1602         $newrecord->timecreated  = $filerecord->timecreated;
1603         $newrecord->timemodified = $filerecord->timemodified;
1604         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1605         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1606         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1607         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1608         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1609         $newrecord->sortorder    = $filerecord->sortorder;
1611         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1612         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1613         // get mimetype by magic bytes
1614         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1616         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1618         try {
1619             $newrecord->id = $DB->insert_record('files', $newrecord);
1620         } catch (dml_exception $e) {
1621             if ($newfile) {
1622                 $this->deleted_file_cleanup($newrecord->contenthash);
1623             }
1624             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1625                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1626         }
1628         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1630         return $this->get_file_instance($newrecord);
1631     }
1633     /**
1634      * Create a new alias/shortcut file from file reference information
1635      *
1636      * @param stdClass|array $filerecord object or array describing the new file
1637      * @param int $repositoryid the id of the repository that provides the original file
1638      * @param string $reference the information required by the repository to locate the original file
1639      * @param array $options options for creating the new file
1640      * @return stored_file
1641      */
1642     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1643         global $DB;
1645         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1646         $filerecord = (object)$filerecord; // We support arrays too.
1648         // validate all parameters, we do not want any rubbish stored in database, right?
1649         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1650             throw new file_exception('storedfileproblem', 'Invalid contextid');
1651         }
1653         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1654         if (empty($filerecord->component)) {
1655             throw new file_exception('storedfileproblem', 'Invalid component');
1656         }
1658         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1659         if (empty($filerecord->filearea)) {
1660             throw new file_exception('storedfileproblem', 'Invalid filearea');
1661         }
1663         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1664             throw new file_exception('storedfileproblem', 'Invalid itemid');
1665         }
1667         if (!empty($filerecord->sortorder)) {
1668             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1669                 $filerecord->sortorder = 0;
1670             }
1671         } else {
1672             $filerecord->sortorder = 0;
1673         }
1675         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1676         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1677         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1678         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1679         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1680         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1681         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1682         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1683             // Path must start and end with '/'.
1684             throw new file_exception('storedfileproblem', 'Invalid file path');
1685         }
1687         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1688         if ($filerecord->filename === '') {
1689             // Path must start and end with '/'.
1690             throw new file_exception('storedfileproblem', 'Invalid file name');
1691         }
1693         $now = time();
1694         if (isset($filerecord->timecreated)) {
1695             if (!is_number($filerecord->timecreated)) {
1696                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1697             }
1698             if ($filerecord->timecreated < 0) {
1699                 // 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)
1700                 $filerecord->timecreated = 0;
1701             }
1702         } else {
1703             $filerecord->timecreated = $now;
1704         }
1706         if (isset($filerecord->timemodified)) {
1707             if (!is_number($filerecord->timemodified)) {
1708                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1709             }
1710             if ($filerecord->timemodified < 0) {
1711                 // 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)
1712                 $filerecord->timemodified = 0;
1713             }
1714         } else {
1715             $filerecord->timemodified = $now;
1716         }
1718         $transaction = $DB->start_delegated_transaction();
1720         try {
1721             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1722         } catch (Exception $e) {
1723             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1724         }
1726         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1727             // there was specified the contenthash for a file already stored in moodle filepool
1728             if (empty($filerecord->filesize)) {
1729                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1730                 $filerecord->filesize = filesize($filepathname);
1731             } else {
1732                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1733             }
1734         } else {
1735             // atempt to get the result of last synchronisation for this reference
1736             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1737                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1738             if ($lastcontent) {
1739                 $filerecord->contenthash = $lastcontent->contenthash;
1740                 $filerecord->filesize = $lastcontent->filesize;
1741             } else {
1742                 // External file doesn't have content in moodle.
1743                 // So we create an empty file for it.
1744                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1745             }
1746         }
1748         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1750         try {
1751             $filerecord->id = $DB->insert_record('files', $filerecord);
1752         } catch (dml_exception $e) {
1753             if (!empty($newfile)) {
1754                 $this->deleted_file_cleanup($filerecord->contenthash);
1755             }
1756             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1757                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1758         }
1760         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1762         $transaction->allow_commit();
1764         // this will retrieve all reference information from DB as well
1765         return $this->get_file_by_id($filerecord->id);
1766     }
1768     /**
1769      * Creates new image file from existing.
1770      *
1771      * @param stdClass|array $filerecord object or array describing new file
1772      * @param int|stored_file $fid file id or stored file object
1773      * @param int $newwidth in pixels
1774      * @param int $newheight in pixels
1775      * @param bool $keepaspectratio whether or not keep aspect ratio
1776      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1777      * @return stored_file
1778      */
1779     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1780         if (!function_exists('imagecreatefromstring')) {
1781             //Most likely the GD php extension isn't installed
1782             //image conversion cannot succeed
1783             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1784         }
1786         if ($fid instanceof stored_file) {
1787             $fid = $fid->get_id();
1788         }
1790         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1792         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1793             throw new file_exception('storedfileproblem', 'File does not exist');
1794         }
1796         if (!$imageinfo = $file->get_imageinfo()) {
1797             throw new file_exception('storedfileproblem', 'File is not an image');
1798         }
1800         if (!isset($filerecord['filename'])) {
1801             $filerecord['filename'] = $file->get_filename();
1802         }
1804         if (!isset($filerecord['mimetype'])) {
1805             $filerecord['mimetype'] = $imageinfo['mimetype'];
1806         }
1808         $width    = $imageinfo['width'];
1809         $height   = $imageinfo['height'];
1811         if ($keepaspectratio) {
1812             if (0 >= $newwidth and 0 >= $newheight) {
1813                 // no sizes specified
1814                 $newwidth  = $width;
1815                 $newheight = $height;
1817             } else if (0 < $newwidth and 0 < $newheight) {
1818                 $xheight = ($newwidth*($height/$width));
1819                 if ($xheight < $newheight) {
1820                     $newheight = (int)$xheight;
1821                 } else {
1822                     $newwidth = (int)($newheight*($width/$height));
1823                 }
1825             } else if (0 < $newwidth) {
1826                 $newheight = (int)($newwidth*($height/$width));
1828             } else { //0 < $newheight
1829                 $newwidth = (int)($newheight*($width/$height));
1830             }
1832         } else {
1833             if (0 >= $newwidth) {
1834                 $newwidth = $width;
1835             }
1836             if (0 >= $newheight) {
1837                 $newheight = $height;
1838             }
1839         }
1841         // The original image.
1842         $img = imagecreatefromstring($file->get_content());
1844         // A new true color image where we will copy our original image.
1845         $newimg = imagecreatetruecolor($newwidth, $newheight);
1847         // Determine if the file supports transparency.
1848         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1850         // Maintain transparency.
1851         if ($hasalpha) {
1852             imagealphablending($newimg, true);
1854             // Get the current transparent index for the original image.
1855             $colour = imagecolortransparent($img);
1856             if ($colour == -1) {
1857                 // Set a transparent colour index if there's none.
1858                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1859                 // Save full alpha channel.
1860                 imagesavealpha($newimg, true);
1861             }
1862             imagecolortransparent($newimg, $colour);
1863             imagefill($newimg, 0, 0, $colour);
1864         }
1866         // Process the image to be output.
1867         if ($height != $newheight or $width != $newwidth) {
1868             // Resample if the dimensions differ from the original.
1869             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1870                 // weird
1871                 throw new file_exception('storedfileproblem', 'Can not resize image');
1872             }
1873             imagedestroy($img);
1874             $img = $newimg;
1876         } else if ($hasalpha) {
1877             // Just copy to the new image with the alpha channel.
1878             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1879                 // Weird.
1880                 throw new file_exception('storedfileproblem', 'Can not copy image');
1881             }
1882             imagedestroy($img);
1883             $img = $newimg;
1885         } else {
1886             // No particular processing needed for the original image.
1887             imagedestroy($newimg);
1888         }
1890         ob_start();
1891         switch ($filerecord['mimetype']) {
1892             case 'image/gif':
1893                 imagegif($img);
1894                 break;
1896             case 'image/jpeg':
1897                 if (is_null($quality)) {
1898                     imagejpeg($img);
1899                 } else {
1900                     imagejpeg($img, NULL, $quality);
1901                 }
1902                 break;
1904             case 'image/png':
1905                 $quality = (int)$quality;
1907                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1908                 // the latter of which can go to 100, we need to make sure that quality here is
1909                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1910                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1911                 imagepng($img, NULL, $quality, NULL);
1912                 break;
1914             default:
1915                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1916         }
1918         $content = ob_get_contents();
1919         ob_end_clean();
1920         imagedestroy($img);
1922         if (!$content) {
1923             throw new file_exception('storedfileproblem', 'Can not convert image');
1924         }
1926         return $this->create_file_from_string($filerecord, $content);
1927     }
1929     /**
1930      * Add file content to sha1 pool.
1931      *
1932      * @param string $pathname path to file
1933      * @param string $contenthash sha1 hash of content if known (performance only)
1934      * @return array (contenthash, filesize, newfile)
1935      */
1936     public function add_file_to_pool($pathname, $contenthash = NULL) {
1937         global $CFG;
1939         if (!is_readable($pathname)) {
1940             throw new file_exception('storedfilecannotread', '', $pathname);
1941         }
1943         $filesize = filesize($pathname);
1944         if ($filesize === false) {
1945             throw new file_exception('storedfilecannotread', '', $pathname);
1946         }
1948         if (is_null($contenthash)) {
1949             $contenthash = sha1_file($pathname);
1950         } else if ($CFG->debugdeveloper) {
1951             $filehash = sha1_file($pathname);
1952             if ($filehash === false) {
1953                 throw new file_exception('storedfilecannotread', '', $pathname);
1954             }
1955             if ($filehash !== $contenthash) {
1956                 // Hopefully this never happens, if yes we need to fix calling code.
1957                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1958                 $contenthash = $filehash;
1959             }
1960         }
1961         if ($contenthash === false) {
1962             throw new file_exception('storedfilecannotread', '', $pathname);
1963         }
1965         if ($filesize > 0 and $contenthash === sha1('')) {
1966             // Did the file change or is sha1_file() borked for this file?
1967             clearstatcache();
1968             $contenthash = sha1_file($pathname);
1969             $filesize = filesize($pathname);
1971             if ($contenthash === false or $filesize === false) {
1972                 throw new file_exception('storedfilecannotread', '', $pathname);
1973             }
1974             if ($filesize > 0 and $contenthash === sha1('')) {
1975                 // This is very weird...
1976                 throw new file_exception('storedfilecannotread', '', $pathname);
1977             }
1978         }
1980         $hashpath = $this->path_from_hash($contenthash);
1981         $hashfile = "$hashpath/$contenthash";
1983         $newfile = true;
1985         if (file_exists($hashfile)) {
1986             if (filesize($hashfile) === $filesize) {
1987                 return array($contenthash, $filesize, false);
1988             }
1989             if (sha1_file($hashfile) === $contenthash) {
1990                 // Jackpot! We have a sha1 collision.
1991                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1992                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1993                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1994                 throw new file_pool_content_exception($contenthash);
1995             }
1996             debugging("Replacing invalid content file $contenthash");
1997             unlink($hashfile);
1998             $newfile = false;
1999         }
2001         if (!is_dir($hashpath)) {
2002             if (!mkdir($hashpath, $this->dirpermissions, true)) {
2003                 // Permission trouble.
2004                 throw new file_exception('storedfilecannotcreatefiledirs');
2005             }
2006         }
2008         // Let's try to prevent some race conditions.
2010         $prev = ignore_user_abort(true);
2011         @unlink($hashfile.'.tmp');
2012         if (!copy($pathname, $hashfile.'.tmp')) {
2013             // Borked permissions or out of disk space.
2014             ignore_user_abort($prev);
2015             throw new file_exception('storedfilecannotcreatefile');
2016         }
2017         if (filesize($hashfile.'.tmp') !== $filesize) {
2018             // This should not happen.
2019             unlink($hashfile.'.tmp');
2020             ignore_user_abort($prev);
2021             throw new file_exception('storedfilecannotcreatefile');
2022         }
2023         rename($hashfile.'.tmp', $hashfile);
2024         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
2025         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
2026         ignore_user_abort($prev);
2028         return array($contenthash, $filesize, $newfile);
2029     }
2031     /**
2032      * Add string content to sha1 pool.
2033      *
2034      * @param string $content file content - binary string
2035      * @return array (contenthash, filesize, newfile)
2036      */
2037     public function add_string_to_pool($content) {
2038         global $CFG;
2040         $contenthash = sha1($content);
2041         $filesize = strlen($content); // binary length
2043         $hashpath = $this->path_from_hash($contenthash);
2044         $hashfile = "$hashpath/$contenthash";
2046         $newfile = true;
2048         if (file_exists($hashfile)) {
2049             if (filesize($hashfile) === $filesize) {
2050                 return array($contenthash, $filesize, false);
2051             }
2052             if (sha1_file($hashfile) === $contenthash) {
2053                 // Jackpot! We have a sha1 collision.
2054                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
2055                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
2056                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
2057                 throw new file_pool_content_exception($contenthash);
2058             }
2059             debugging("Replacing invalid content file $contenthash");
2060             unlink($hashfile);
2061             $newfile = false;
2062         }
2064         if (!is_dir($hashpath)) {
2065             if (!mkdir($hashpath, $this->dirpermissions, true)) {
2066                 // Permission trouble.
2067                 throw new file_exception('storedfilecannotcreatefiledirs');
2068             }
2069         }
2071         // Hopefully this works around most potential race conditions.
2073         $prev = ignore_user_abort(true);
2075         if (!empty($CFG->preventfilelocking)) {
2076             $newsize = file_put_contents($hashfile.'.tmp', $content);
2077         } else {
2078             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
2079         }
2081         if ($newsize === false) {
2082             // Borked permissions most likely.
2083             ignore_user_abort($prev);
2084             throw new file_exception('storedfilecannotcreatefile');
2085         }
2086         if (filesize($hashfile.'.tmp') !== $filesize) {
2087             // Out of disk space?
2088             unlink($hashfile.'.tmp');
2089             ignore_user_abort($prev);
2090             throw new file_exception('storedfilecannotcreatefile');
2091         }
2092         rename($hashfile.'.tmp', $hashfile);
2093         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
2094         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
2095         ignore_user_abort($prev);
2097         return array($contenthash, $filesize, $newfile);
2098     }
2100     /**
2101      * Serve file content using X-Sendfile header.
2102      * Please make sure that all headers are already sent
2103      * and the all access control checks passed.
2104      *
2105      * @param string $contenthash sah1 hash of the file content to be served
2106      * @return bool success
2107      */
2108     public function xsendfile($contenthash) {
2109         global $CFG;
2110         require_once("$CFG->libdir/xsendfilelib.php");
2112         $hashpath = $this->path_from_hash($contenthash);
2113         return xsendfile("$hashpath/$contenthash");
2114     }
2116     /**
2117      * Content exists
2118      *
2119      * @param string $contenthash
2120      * @return bool
2121      */
2122     public function content_exists($contenthash) {
2123         $dir = $this->path_from_hash($contenthash);
2124         $filepath = $dir . '/' . $contenthash;
2125         return file_exists($filepath);
2126     }
2128     /**
2129      * Return path to file with given hash.
2130      *
2131      * NOTE: must not be public, files in pool must not be modified
2132      *
2133      * @param string $contenthash content hash
2134      * @return string expected file location
2135      */
2136     protected function path_from_hash($contenthash) {
2137         $l1 = $contenthash[0].$contenthash[1];
2138         $l2 = $contenthash[2].$contenthash[3];
2139         return "$this->filedir/$l1/$l2";
2140     }
2142     /**
2143      * Return path to file with given hash.
2144      *
2145      * NOTE: must not be public, files in pool must not be modified
2146      *
2147      * @param string $contenthash content hash
2148      * @return string expected file location
2149      */
2150     protected function trash_path_from_hash($contenthash) {
2151         $l1 = $contenthash[0].$contenthash[1];
2152         $l2 = $contenthash[2].$contenthash[3];
2153         return "$this->trashdir/$l1/$l2";
2154     }
2156     /**
2157      * Tries to recover missing content of file from trash.
2158      *
2159      * @param stored_file $file stored_file instance
2160      * @return bool success
2161      */
2162     public function try_content_recovery($file) {
2163         $contenthash = $file->get_contenthash();
2164         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
2165         if (!is_readable($trashfile)) {
2166             if (!is_readable($this->trashdir.'/'.$contenthash)) {
2167                 return false;
2168             }
2169             // nice, at least alternative trash file in trash root exists
2170             $trashfile = $this->trashdir.'/'.$contenthash;
2171         }
2172         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
2173             //weird, better fail early
2174             return false;
2175         }
2176         $contentdir  = $this->path_from_hash($contenthash);
2177         $contentfile = $contentdir.'/'.$contenthash;
2178         if (file_exists($contentfile)) {
2179             //strange, no need to recover anything
2180             return true;
2181         }
2182         if (!is_dir($contentdir)) {
2183             if (!mkdir($contentdir, $this->dirpermissions, true)) {
2184                 return false;
2185             }
2186         }
2187         return rename($trashfile, $contentfile);
2188     }
2190     /**
2191      * Marks pool file as candidate for deleting.
2192      *
2193      * DO NOT call directly - reserved for core!!
2194      *
2195      * @param string $contenthash
2196      */
2197     public function deleted_file_cleanup($contenthash) {
2198         global $DB;
2200         if ($contenthash === sha1('')) {
2201             // No need to delete empty content file with sha1('') content hash.
2202             return;
2203         }
2205         //Note: this section is critical - in theory file could be reused at the same
2206         //      time, if this happens we can still recover the file from trash
2207         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
2208             // file content is still used
2209             return;
2210         }
2211         //move content file to trash
2212         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
2213         if (!file_exists($contentfile)) {
2214             //weird, but no problem
2215             return;
2216         }
2217         $trashpath = $this->trash_path_from_hash($contenthash);
2218         $trashfile = $trashpath.'/'.$contenthash;
2219         if (file_exists($trashfile)) {
2220             // we already have this content in trash, no need to move it there
2221             unlink($contentfile);
2222             return;
2223         }
2224         if (!is_dir($trashpath)) {
2225             mkdir($trashpath, $this->dirpermissions, true);
2226         }
2227         rename($contentfile, $trashfile);
2229         // Fix permissions, only if needed.
2230         $currentperms = octdec(substr(decoct(fileperms($trashfile)), -4));
2231         if ((int)$this->filepermissions !== $currentperms) {
2232             chmod($trashfile, $this->filepermissions);
2233         }
2234     }
2236     /**
2237      * When user referring to a moodle file, we build the reference field
2238      *
2239      * @param array $params
2240      * @return string
2241      */
2242     public static function pack_reference($params) {
2243         $params = (array)$params;
2244         $reference = array();
2245         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
2246         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
2247         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
2248         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
2249         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
2250         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
2251         return base64_encode(serialize($reference));
2252     }
2254     /**
2255      * Unpack reference field
2256      *
2257      * @param string $str
2258      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
2259      * @throws file_reference_exception if the $str does not have the expected format
2260      * @return array
2261      */
2262     public static function unpack_reference($str, $cleanparams = false) {
2263         $decoded = base64_decode($str, true);
2264         if ($decoded === false) {
2265             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
2266         }
2267         $params = @unserialize($decoded); // hide E_NOTICE
2268         if ($params === false) {
2269             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
2270         }
2271         if (is_array($params) && $cleanparams) {
2272             $params = array(
2273                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
2274                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
2275                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
2276                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
2277                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
2278                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
2279             );
2280         }
2281         return $params;
2282     }
2284     /**
2285      * Search through the server files.
2286      *
2287      * The query parameter will be used in conjuction with the SQL directive
2288      * LIKE, so include '%' in it if you need to. This search will always ignore
2289      * user files and directories. Note that the search is case insensitive.
2290      *
2291      * This query can quickly become inefficient so use it sparignly.
2292      *
2293      * @param  string  $query The string used with SQL LIKE.
2294      * @param  integer $from  The offset to start the search at.
2295      * @param  integer $limit The maximum number of results.
2296      * @param  boolean $count When true this methods returns the number of results availabe,
2297      *                        disregarding the parameters $from and $limit.
2298      * @return int|array      Integer when count, otherwise array of stored_file objects.
2299      */
2300     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
2301         global $DB;
2302         $params = array(
2303             'contextlevel' => CONTEXT_USER,
2304             'directory' => '.',
2305             'query' => $query
2306         );
2308         if ($count) {
2309             $select = 'COUNT(1)';
2310         } else {
2311             $select = self::instance_sql_fields('f', 'r');
2312         }
2313         $like = $DB->sql_like('f.filename', ':query', false);
2315         $sql = "SELECT $select
2316                   FROM {files} f
2317              LEFT JOIN {files_reference} r
2318                     ON f.referencefileid = r.id
2319                   JOIN {context} c
2320                     ON f.contextid = c.id
2321                  WHERE c.contextlevel <> :contextlevel
2322                    AND f.filename <> :directory
2323                    AND " . $like . "";
2325         if ($count) {
2326             return $DB->count_records_sql($sql, $params);
2327         }
2329         $sql .= " ORDER BY f.filename";
2331         $result = array();
2332         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2333         foreach ($filerecords as $filerecord) {
2334             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2335         }
2336         $filerecords->close();
2338         return $result;
2339     }
2341     /**
2342      * Returns all aliases that refer to some stored_file via the given reference
2343      *
2344      * All repositories that provide access to a stored_file are expected to use
2345      * {@link self::pack_reference()}. This method can't be used if the given reference
2346      * does not use this format or if you are looking for references to an external file
2347      * (for example it can't be used to search for all aliases that refer to a given
2348      * Dropbox or Box.net file).
2349      *
2350      * Aliases in user draft areas are excluded from the returned list.
2351      *
2352      * @param string $reference identification of the referenced file
2353      * @return array of stored_file indexed by its pathnamehash
2354      */
2355     public function search_references($reference) {
2356         global $DB;
2358         if (is_null($reference)) {
2359             throw new coding_exception('NULL is not a valid reference to an external file');
2360         }
2362         // Give {@link self::unpack_reference()} a chance to throw exception if the
2363         // reference is not in a valid format.
2364         self::unpack_reference($reference);
2366         $referencehash = sha1($reference);
2368         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2369                   FROM {files} f
2370                   JOIN {files_reference} r ON f.referencefileid = r.id
2371                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2372                  WHERE r.referencehash = ?
2373                        AND (f.component <> ? OR f.filearea <> ?)";
2375         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2376         $files = array();
2377         foreach ($rs as $filerecord) {
2378             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2379         }
2381         return $files;
2382     }
2384     /**
2385      * Returns the number of aliases that refer to some stored_file via the given reference
2386      *
2387      * All repositories that provide access to a stored_file are expected to use
2388      * {@link self::pack_reference()}. This method can't be used if the given reference
2389      * does not use this format or if you are looking for references to an external file
2390      * (for example it can't be used to count aliases that refer to a given Dropbox or
2391      * Box.net file).
2392      *
2393      * Aliases in user draft areas are not counted.
2394      *
2395      * @param string $reference identification of the referenced file
2396      * @return int
2397      */
2398     public function search_references_count($reference) {
2399         global $DB;
2401         if (is_null($reference)) {
2402             throw new coding_exception('NULL is not a valid reference to an external file');
2403         }
2405         // Give {@link self::unpack_reference()} a chance to throw exception if the
2406         // reference is not in a valid format.
2407         self::unpack_reference($reference);
2409         $referencehash = sha1($reference);
2411         $sql = "SELECT COUNT(f.id)
2412                   FROM {files} f
2413                   JOIN {files_reference} r ON f.referencefileid = r.id
2414                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2415                  WHERE r.referencehash = ?
2416                        AND (f.component <> ? OR f.filearea <> ?)";
2418         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2419     }
2421     /**
2422      * Returns all aliases that link to the given stored_file
2423      *
2424      * Aliases in user draft areas are excluded from the returned list.
2425      *
2426      * @param stored_file $storedfile
2427      * @return array of stored_file
2428      */
2429     public function get_references_by_storedfile(stored_file $storedfile) {
2430         global $DB;
2432         $params = array();
2433         $params['contextid'] = $storedfile->get_contextid();
2434         $params['component'] = $storedfile->get_component();
2435         $params['filearea']  = $storedfile->get_filearea();
2436         $params['itemid']    = $storedfile->get_itemid();
2437         $params['filename']  = $storedfile->get_filename();
2438         $params['filepath']  = $storedfile->get_filepath();
2440         return $this->search_references(self::pack_reference($params));
2441     }
2443     /**
2444      * Returns the number of aliases that link to the given stored_file
2445      *
2446      * Aliases in user draft areas are not counted.
2447      *
2448      * @param stored_file $storedfile
2449      * @return int
2450      */
2451     public function get_references_count_by_storedfile(stored_file $storedfile) {
2452         global $DB;
2454         $params = array();
2455         $params['contextid'] = $storedfile->get_contextid();
2456         $params['component'] = $storedfile->get_component();
2457         $params['filearea']  = $storedfile->get_filearea();
2458         $params['itemid']    = $storedfile->get_itemid();
2459         $params['filename']  = $storedfile->get_filename();
2460         $params['filepath']  = $storedfile->get_filepath();
2462         return $this->search_references_count(self::pack_reference($params));
2463     }
2465     /**
2466      * Updates all files that are referencing this file with the new contenthash
2467      * and filesize
2468      *
2469      * @param stored_file $storedfile
2470      */
2471     public function update_references_to_storedfile(stored_file $storedfile) {
2472         global $CFG, $DB;
2473         $params = array();
2474         $params['contextid'] = $storedfile->get_contextid();
2475         $params['component'] = $storedfile->get_component();
2476         $params['filearea']  = $storedfile->get_filearea();
2477         $params['itemid']    = $storedfile->get_itemid();
2478         $params['filename']  = $storedfile->get_filename();
2479         $params['filepath']  = $storedfile->get_filepath();
2480         $reference = self::pack_reference($params);
2481         $referencehash = sha1($reference);
2483         $sql = "SELECT repositoryid, id FROM {files_reference}
2484                  WHERE referencehash = ?";
2485         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2487         $now = time();
2488         foreach ($rs as $record) {
2489             $this->update_references($record->id, $now, null,
2490                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2491         }
2492         $rs->close();
2493     }
2495     /**
2496      * Convert file alias to local file
2497      *
2498      * @throws moodle_exception if file could not be downloaded
2499      *
2500      * @param stored_file $storedfile a stored_file instances
2501      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2502      * @return stored_file stored_file
2503      */
2504     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2505         global $CFG;
2506         $storedfile->import_external_file_contents($maxbytes);
2507         $storedfile->delete_reference();
2508         return $storedfile;
2509     }
2511     /**
2512      * Return mimetype by given file pathname
2513      *
2514      * If file has a known extension, we return the mimetype based on extension.
2515      * Otherwise (when possible) we try to get the mimetype from file contents.
2516      *
2517      * @param string $pathname full path to the file
2518      * @param string $filename correct file name with extension, if omitted will be taken from $path
2519      * @return string
2520      */
2521     public static function mimetype($pathname, $filename = null) {
2522         if (empty($filename)) {
2523             $filename = $pathname;
2524         }
2525         $type = mimeinfo('type', $filename);
2526         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2527             $finfo = new finfo(FILEINFO_MIME_TYPE);
2528             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2529         }
2530         return $type;
2531     }
2533     /**
2534      * Cron cleanup job.
2535      */
2536     public function cron() {
2537         global $CFG, $DB;
2538         require_once($CFG->libdir.'/cronlib.php');
2540         // find out all stale draft areas (older than 4 days) and purge them
2541         // those are identified by time stamp of the /. root dir
2542         mtrace('Deleting old draft files... ', '');
2543         cron_trace_time_and_memory();
2544         $old = time() - 60*60*24*4;
2545         $sql = "SELECT *
2546                   FROM {files}
2547                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2548                        AND timecreated < :old";
2549         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2550         foreach ($rs as $dir) {
2551             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2552         }
2553         $rs->close();
2554         mtrace('done.');
2556         // remove orphaned preview files (that is files in the core preview filearea without
2557         // the existing original file)
2558         mtrace('Deleting orphaned preview files... ', '');
2559         cron_trace_time_and_memory();
2560         $sql = "SELECT p.*
2561                   FROM {files} p
2562              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2563                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2564                        AND o.id IS NULL";
2565         $syscontext = context_system::instance();
2566         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2567         foreach ($rs as $orphan) {
2568             $file = $this->get_file_instance($orphan);
2569             if (!$file->is_directory()) {
2570                 $file->delete();
2571             }
2572         }
2573         $rs->close();
2574         mtrace('done.');
2576         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2577         // the existing original file).
2578         mtrace('Deleting orphaned document conversion files... ', '');
2579         cron_trace_time_and_memory();
2580         $sql = "SELECT p.*
2581                   FROM {files} p
2582              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2583                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
2584                        AND o.id IS NULL";
2585         $syscontext = context_system::instance();
2586         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2587         foreach ($rs as $orphan) {
2588             $file = $this->get_file_instance($orphan);
2589             if (!$file->is_directory()) {
2590                 $file->delete();
2591             }
2592         }
2593         $rs->close();
2594         mtrace('done.');
2596         // remove trash pool files once a day
2597         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2598         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2599             require_once($CFG->libdir.'/filelib.php');
2600             // Delete files that are associated with a context that no longer exists.
2601             mtrace('Cleaning up files from deleted contexts... ', '');
2602             cron_trace_time_and_memory();
2603             $sql = "SELECT DISTINCT f.contextid
2604                     FROM {files} f
2605                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2606                     WHERE c.id IS NULL";
2607             $rs = $DB->get_recordset_sql($sql);
2608             if ($rs->valid()) {
2609                 $fs = get_file_storage();
2610                 foreach ($rs as $ctx) {
2611                     $fs->delete_area_files($ctx->contextid);
2612                 }
2613             }
2614             $rs->close();
2615             mtrace('done.');
2617             mtrace('Deleting trash files... ', '');
2618             cron_trace_time_and_memory();
2619             fulldelete($this->trashdir);
2620             set_config('fileslastcleanup', time());
2621             mtrace('done.');
2622         }
2623     }
2625     /**
2626      * Get the sql formated fields for a file instance to be created from a
2627      * {files} and {files_refernece} join.
2628      *
2629      * @param string $filesprefix the table prefix for the {files} table
2630      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2631      * @return string the sql to go after a SELECT
2632      */
2633     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2634         // Note, these fieldnames MUST NOT overlap between the two tables,
2635         // else problems like MDL-33172 occur.
2636         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2637             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2638             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2640         $referencefields = array('repositoryid' => 'repositoryid',
2641             'reference' => 'reference',
2642             'lastsync' => 'referencelastsync');
2644         // id is specifically named to prevent overlaping between the two tables.
2645         $fields = array();
2646         $fields[] = $filesprefix.'.id AS id';
2647         foreach ($filefields as $field) {
2648             $fields[] = "{$filesprefix}.{$field}";
2649         }
2651         foreach ($referencefields as $field => $alias) {
2652             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2653         }
2655         return implode(', ', $fields);
2656     }
2658     /**
2659      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2660      *
2661      * If the record already exists, its id is returned. If there is no such record yet,
2662      * new one is created (using the lastsync provided, too) and its id is returned.
2663      *
2664      * @param int $repositoryid
2665      * @param string $reference
2666      * @param int $lastsync
2667      * @param int $lifetime argument not used any more
2668      * @return int
2669      */
2670     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2671         global $DB;
2673         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2675         if ($id !== false) {
2676             // bah, that was easy
2677             return $id;
2678         }
2680         // no such record yet, create one
2681         try {
2682             $id = $DB->insert_record('files_reference', array(
2683                 'repositoryid'  => $repositoryid,
2684                 'reference'     => $reference,
2685                 'referencehash' => sha1($reference),
2686                 'lastsync'      => $lastsync));
2687         } catch (dml_exception $e) {
2688             // if inserting the new record failed, chances are that the race condition has just
2689             // occured and the unique index did not allow to create the second record with the same
2690             // repositoryid + reference combo
2691             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2692         }
2694         return $id;
2695     }
2697     /**
2698      * Returns the id of the record in {files_reference} that matches the passed parameters
2699      *
2700      * Depending on the required strictness, false can be returned. The behaviour is consistent
2701      * with standard DML methods.
2702      *
2703      * @param int $repositoryid
2704      * @param string $reference
2705      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2706      * @return int|bool
2707      */
2708     private function get_referencefileid($repositoryid, $reference, $strictness) {
2709         global $DB;
2711         return $DB->get_field('files_reference', 'id',
2712             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2713     }
2715     /**
2716      * Updates a reference to the external resource and all files that use it
2717      *
2718      * This function is called after synchronisation of an external file and updates the
2719      * contenthash, filesize and status of all files that reference this external file
2720      * as well as time last synchronised.
2721      *
2722      * @param int $referencefileid
2723      * @param int $lastsync
2724      * @param int $lifetime argument not used any more, liefetime is returned by repository
2725      * @param string $contenthash
2726      * @param int $filesize
2727      * @param int $status 0 if ok or 666 if source is missing
2728      * @param int $timemodified last time modified of the source, if known
2729      */
2730     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2731         global $DB;
2732         $referencefileid = clean_param($referencefileid, PARAM_INT);
2733         $lastsync = clean_param($lastsync, PARAM_INT);
2734         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2735         $filesize = clean_param($filesize, PARAM_INT);
2736         $status = clean_param($status, PARAM_INT);
2737         $params = array('contenthash' => $contenthash,
2738                     'filesize' => $filesize,
2739                     'status' => $status,
2740                     'referencefileid' => $referencefileid,
2741                     'timemodified' => $timemodified);
2742         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2743             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2744             WHERE referencefileid = :referencefileid', $params);
2745         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2746         $DB->update_record('files_reference', (object)$data);
2747     }