Merge branch 'MDL-55513-master' of https://github.com/lucisgit/moodle
[moodle.git] / lib / filestorage / file_storage.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Core file storage class definition.
20  *
21  * @package   core_files
22  * @copyright 2008 Petr Skoda {@link http://skodak.org}
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/stored_file.php");
30 /**
31  * File storage class used for low level access to stored files.
32  *
33  * Only owner of file area may use this class to access own files,
34  * for example only code in mod/assignment/* may access assignment
35  * attachments. When some other part of moodle needs to access
36  * files of modules it has to use file_browser class instead or there
37  * has to be some callback API.
38  *
39  * @package   core_files
40  * @category  files
41  * @copyright 2008 Petr Skoda {@link http://skodak.org}
42  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  * @since     Moodle 2.0
44  */
45 class file_storage {
46     /** @var string Directory with file contents */
47     private $filedir;
48     /** @var string Contents of deleted files not needed any more */
49     private $trashdir;
50     /** @var string tempdir */
51     private $tempdir;
52     /** @var int Permissions for new directories */
53     private $dirpermissions;
54     /** @var int Permissions for new files */
55     private $filepermissions;
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      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
188      */
189     public function get_converted_document(stored_file $file, $format) {
191         $context = context_system::instance();
192         $path = '/' . $format . '/';
193         $conversion = $this->get_file($context->id, 'core', 'documentconversion', 0, $path, $file->get_contenthash());
195         if (!$conversion) {
196             $conversion = $this->create_converted_document($file, $format);
197             if (!$conversion) {
198                 return false;
199             }
200         }
202         return $conversion;
203     }
205     /**
206      * Verify the format is supported.
207      *
208      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
209      * @return bool - True if the format is supported for input.
210      */
211     protected function is_format_supported_by_unoconv($format) {
212         global $CFG;
214         if (!isset($this->unoconvformats)) {
215             // Ask unoconv for it's list of supported document formats.
216             $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' --show';
217             $pipes = array();
218             $pipesspec = array(2 => array('pipe', 'w'));
219             $proc = proc_open($cmd, $pipesspec, $pipes);
220             $programoutput = stream_get_contents($pipes[2]);
221             fclose($pipes[2]);
222             proc_close($proc);
223             $matches = array();
224             preg_match_all('/\[\.(.*)\]/', $programoutput, $matches);
226             $this->unoconvformats = $matches[1];
227             $this->unoconvformats = array_unique($this->unoconvformats);
228         }
230         $sanitized = trim(core_text::strtolower($format));
231         return in_array($sanitized, $this->unoconvformats);
232     }
234     /**
235      * Check if the installed version of unoconv is supported.
236      *
237      * @return bool true if the present version is supported, false otherwise.
238      */
239     public static function can_convert_documents() {
240         global $CFG;
241         $currentversion = 0;
242         $supportedversion = 0.7;
243         $unoconvbin = \escapeshellarg($CFG->pathtounoconv);
244         $command = "$unoconvbin --version";
245         exec($command, $output);
246         // If the command execution returned some output, then get the unoconv version.
247         if ($output) {
248             foreach ($output as $response) {
249                 if (preg_match('/unoconv (\\d+\\.\\d+)/', $response, $matches)) {
250                     $currentversion = (float)$matches[1];
251                 }
252             }
253             if ($currentversion < $supportedversion) {
254                 return false;
255             }
256             return true;
257         }
258         return false;
259     }
261     /**
262      * If the test pdf has been generated correctly and send it direct to the browser.
263      */
264     public static function send_test_pdf() {
265         global $CFG;
266         require_once($CFG->libdir . '/filelib.php');
268         $filerecord = array(
269             'contextid' => \context_system::instance()->id,
270             'component' => 'test',
271             'filearea' => 'assignfeedback_editpdf',
272             'itemid' => 0,
273             'filepath' => '/',
274             'filename' => 'unoconv_test.docx'
275         );
277         // Get the fixture doc file content and generate and stored_file object.
278         $fs = get_file_storage();
279         $fixturefile = $CFG->libdir . '/tests/fixtures/unoconv-source.docx';
280         $fixturedata = file_get_contents($fixturefile);
281         $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'],
282                 $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);
283         if (!$testdocx) {
284             $testdocx = $fs->create_file_from_string($filerecord, $fixturedata);
286         }
288         // Convert the doc file to pdf and send it direct to the browser.
289         $result = $fs->get_converted_document($testdocx, 'pdf');
290         readfile_accel($result, 'application/pdf', true);
291     }
293     /**
294      * Check if unoconv configured path is correct and working.
295      *
296      * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
297      */
298     public static function test_unoconv_path() {
299         global $CFG;
300         $unoconvpath = $CFG->pathtounoconv;
302         $ret = new \stdClass();
303         $ret->status = self::UNOCONVPATH_OK;
304         $ret->message = null;
306         if (empty($unoconvpath)) {
307             $ret->status = self::UNOCONVPATH_EMPTY;
308             return $ret;
309         }
310         if (!file_exists($unoconvpath)) {
311             $ret->status = self::UNOCONVPATH_DOESNOTEXIST;
312             return $ret;
313         }
314         if (is_dir($unoconvpath)) {
315             $ret->status = self::UNOCONVPATH_ISDIR;
316             return $ret;
317         }
318         if (!file_is_executable($unoconvpath)) {
319             $ret->status = self::UNOCONVPATH_NOTEXECUTABLE;
320             return $ret;
321         }
322         if (!\file_storage::can_convert_documents()) {
323             $ret->status = self::UNOCONVPATH_VERSIONNOTSUPPORTED;
324             return $ret;
325         }
327         return $ret;
328     }
330     /**
331      * Perform a file format conversion on the specified document.
332      *
333      * @param stored_file $file the file we want to preview
334      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
335      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
336      */
337     protected function create_converted_document(stored_file $file, $format) {
338         global $CFG;
340         if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
341             // No conversions are possible, sorry.
342             return false;
343         }
345         $fileextension = core_text::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
346         if (!self::is_format_supported_by_unoconv($fileextension)) {
347             return false;
348         }
350         if (!self::is_format_supported_by_unoconv($format)) {
351             return false;
352         }
354         // Copy the file to the tmp dir.
355         $uniqdir = "core_file/conversions/" . uniqid($file->get_id() . "-", true);
356         $tmp = make_temp_directory($uniqdir);
357         $ext = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
358         // Safety.
359         $localfilename = $file->get_id() . '.' . $ext;
361         $filename = $tmp . '/' . $localfilename;
362         try {
363             // This function can either return false, or throw an exception so we need to handle both.
364             if ($file->copy_content_to($filename) === false) {
365                 throw new file_exception('storedfileproblem', 'Could not copy file contents to temp file.');
366             }
367         } catch (file_exception $fe) {
368             remove_dir($tmp);
369             throw $fe;
370         }
372         $newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format;
374         // Safety.
375         $newtmpfile = $tmp . '/' . clean_param($newtmpfile, PARAM_FILE);
377         $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' .
378                escapeshellarg('-f') . ' ' .
379                escapeshellarg($format) . ' ' .
380                escapeshellarg('-o') . ' ' .
381                escapeshellarg($newtmpfile) . ' ' .
382                escapeshellarg($filename);
384         $output = null;
385         $currentdir = getcwd();
386         chdir($tmp);
387         $result = exec($cmd, $output);
388         chdir($currentdir);
389         touch($newtmpfile);
390         if (filesize($newtmpfile) === 0) {
391             remove_dir($tmp);
392             // Cleanup.
393             return false;
394         }
396         $context = context_system::instance();
397         $record = array(
398             'contextid' => $context->id,
399             'component' => 'core',
400             'filearea'  => 'documentconversion',
401             'itemid'    => 0,
402             'filepath'  => '/' . $format . '/',
403             'filename'  => $file->get_contenthash(),
404         );
406         $convertedfile = $this->create_file_from_pathname($record, $newtmpfile);
407         // Cleanup.
408         remove_dir($tmp);
409         return $convertedfile;
410     }
412     /**
413      * Returns an image file that represent the given stored file as a preview
414      *
415      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
416      * future, the support for other mimetypes can be added, too (eg. generate an image
417      * preview of PDF, text documents etc).
418      *
419      * @param stored_file $file the file we want to preview
420      * @param string $mode preview mode, eg. 'thumb'
421      * @return stored_file|bool false if unable to create the preview, stored file otherwise
422      */
423     public function get_file_preview(stored_file $file, $mode) {
425         $context = context_system::instance();
426         $path = '/' . trim($mode, '/') . '/';
427         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
429         if (!$preview) {
430             $preview = $this->create_file_preview($file, $mode);
431             if (!$preview) {
432                 return false;
433             }
434         }
436         return $preview;
437     }
439     /**
440      * Return an available file name.
441      *
442      * This will return the next available file name in the area, adding/incrementing a suffix
443      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
444      *
445      * If the file name passed is available without modification, it is returned as is.
446      *
447      * @param int $contextid context ID.
448      * @param string $component component.
449      * @param string $filearea file area.
450      * @param int $itemid area item ID.
451      * @param string $filepath the file path.
452      * @param string $filename the file name.
453      * @return string available file name.
454      * @throws coding_exception if the file name is invalid.
455      * @since Moodle 2.5
456      */
457     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
458         global $DB;
460         // Do not accept '.' or an empty file name (zero is acceptable).
461         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
462             throw new coding_exception('Invalid file name passed', $filename);
463         }
465         // The file does not exist, we return the same file name.
466         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
467             return $filename;
468         }
470         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
471         $pathinfo = pathinfo($filename);
472         $basename = $pathinfo['filename'];
473         $matches = array();
474         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
475             $basename = $matches[1];
476         }
478         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
479         if (isset($pathinfo['extension'])) {
480             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
481         }
483         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
484         $filenamelen = $DB->sql_length('f.filename');
485         $sql = "SELECT filename
486                 FROM {files} f
487                 WHERE
488                     f.contextid = :contextid AND
489                     f.component = :component AND
490                     f.filearea = :filearea AND
491                     f.itemid = :itemid AND
492                     f.filepath = :filepath AND
493                     $filenamelikesql
494                 ORDER BY
495                     $filenamelen DESC,
496                     f.filename DESC";
497         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
498                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
499         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
501         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
502         // would both be returned, but only the one only containing digits should be used.
503         $number = 1;
504         foreach ($results as $result) {
505             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
506             $matches = array();
507             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
508                 $number = $matches[2] + 1;
509                 break;
510             }
511         }
513         // Constructing the new filename.
514         $newfilename = $basename . ' (' . $number . ')';
515         if (isset($pathinfo['extension'])) {
516             $newfilename .= '.' . $pathinfo['extension'];
517         }
519         return $newfilename;
520     }
522     /**
523      * Return an available directory name.
524      *
525      * This will return the next available directory name in the area, adding/incrementing a suffix
526      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
527      *
528      * If the file path passed is available without modification, it is returned as is.
529      *
530      * @param int $contextid context ID.
531      * @param string $component component.
532      * @param string $filearea file area.
533      * @param int $itemid area item ID.
534      * @param string $suggestedpath the suggested file path.
535      * @return string available file path
536      * @since Moodle 2.5
537      */
538     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
539         global $DB;
541         // Ensure suggestedpath has trailing '/'
542         $suggestedpath = rtrim($suggestedpath, '/'). '/';
544         // The directory does not exist, we return the same file path.
545         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
546             return $suggestedpath;
547         }
549         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
550         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
551             $suggestedpath = $matches[1]. '/';
552         }
554         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
556         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
557         $filepathlen = $DB->sql_length('f.filepath');
558         $sql = "SELECT filepath
559                 FROM {files} f
560                 WHERE
561                     f.contextid = :contextid AND
562                     f.component = :component AND
563                     f.filearea = :filearea AND
564                     f.itemid = :itemid AND
565                     f.filename = :filename AND
566                     $filepathlikesql
567                 ORDER BY
568                     $filepathlen DESC,
569                     f.filepath DESC";
570         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
571                 'filename' => '.', 'filepathlike' => $filepathlike);
572         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
574         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
575         // would both be returned, but only the one only containing digits should be used.
576         $number = 1;
577         foreach ($results as $result) {
578             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
579                 $number = (int)($matches[1]) + 1;
580                 break;
581             }
582         }
584         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
585     }
587     /**
588      * Generates a preview image for the stored file
589      *
590      * @param stored_file $file the file we want to preview
591      * @param string $mode preview mode, eg. 'thumb'
592      * @return stored_file|bool the newly created preview file or false
593      */
594     protected function create_file_preview(stored_file $file, $mode) {
596         $mimetype = $file->get_mimetype();
598         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
599             // make a preview of the image
600             $data = $this->create_imagefile_preview($file, $mode);
602         } else {
603             // unable to create the preview of this mimetype yet
604             return false;
605         }
607         if (empty($data)) {
608             return false;
609         }
611         $context = context_system::instance();
612         $record = array(
613             'contextid' => $context->id,
614             'component' => 'core',
615             'filearea'  => 'preview',
616             'itemid'    => 0,
617             'filepath'  => '/' . trim($mode, '/') . '/',
618             'filename'  => $file->get_contenthash(),
619         );
621         $imageinfo = getimagesizefromstring($data);
622         if ($imageinfo) {
623             $record['mimetype'] = $imageinfo['mime'];
624         }
626         return $this->create_file_from_string($record, $data);
627     }
629     /**
630      * Generates a preview for the stored image file
631      *
632      * @param stored_file $file the image we want to preview
633      * @param string $mode preview mode, eg. 'thumb'
634      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
635      */
636     protected function create_imagefile_preview(stored_file $file, $mode) {
637         global $CFG;
638         require_once($CFG->libdir.'/gdlib.php');
640         if ($mode === 'tinyicon') {
641             $data = $file->generate_image_thumbnail(24, 24);
643         } else if ($mode === 'thumb') {
644             $data = $file->generate_image_thumbnail(90, 90);
646         } else if ($mode === 'bigthumb') {
647             $data = $file->generate_image_thumbnail(250, 250);
649         } else {
650             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
651         }
653         return $data;
654     }
656     /**
657      * Fetch file using local file id.
658      *
659      * Please do not rely on file ids, it is usually easier to use
660      * pathname hashes instead.
661      *
662      * @param int $fileid file ID
663      * @return stored_file|bool stored_file instance if exists, false if not
664      */
665     public function get_file_by_id($fileid) {
666         global $DB;
668         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
669                   FROM {files} f
670              LEFT JOIN {files_reference} r
671                        ON f.referencefileid = r.id
672                  WHERE f.id = ?";
673         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
674             return $this->get_file_instance($filerecord);
675         } else {
676             return false;
677         }
678     }
680     /**
681      * Fetch file using local file full pathname hash
682      *
683      * @param string $pathnamehash path name hash
684      * @return stored_file|bool stored_file instance if exists, false if not
685      */
686     public function get_file_by_hash($pathnamehash) {
687         global $DB;
689         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
690                   FROM {files} f
691              LEFT JOIN {files_reference} r
692                        ON f.referencefileid = r.id
693                  WHERE f.pathnamehash = ?";
694         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
695             return $this->get_file_instance($filerecord);
696         } else {
697             return false;
698         }
699     }
701     /**
702      * Fetch locally stored file.
703      *
704      * @param int $contextid context ID
705      * @param string $component component
706      * @param string $filearea file area
707      * @param int $itemid item ID
708      * @param string $filepath file path
709      * @param string $filename file name
710      * @return stored_file|bool stored_file instance if exists, false if not
711      */
712     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
713         $filepath = clean_param($filepath, PARAM_PATH);
714         $filename = clean_param($filename, PARAM_FILE);
716         if ($filename === '') {
717             $filename = '.';
718         }
720         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
721         return $this->get_file_by_hash($pathnamehash);
722     }
724     /**
725      * Are there any files (or directories)
726      *
727      * @param int $contextid context ID
728      * @param string $component component
729      * @param string $filearea file area
730      * @param bool|int $itemid item id or false if all items
731      * @param bool $ignoredirs whether or not ignore directories
732      * @return bool empty
733      */
734     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
735         global $DB;
737         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
738         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
740         if ($itemid !== false) {
741             $params['itemid'] = $itemid;
742             $where .= " AND itemid = :itemid";
743         }
745         if ($ignoredirs) {
746             $sql = "SELECT 'x'
747                       FROM {files}
748                      WHERE $where AND filename <> '.'";
749         } else {
750             $sql = "SELECT 'x'
751                       FROM {files}
752                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
753         }
755         return !$DB->record_exists_sql($sql, $params);
756     }
758     /**
759      * Returns all files belonging to given repository
760      *
761      * @param int $repositoryid
762      * @param string $sort A fragment of SQL to use for sorting
763      */
764     public function get_external_files($repositoryid, $sort = '') {
765         global $DB;
766         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
767                   FROM {files} f
768              LEFT JOIN {files_reference} r
769                        ON f.referencefileid = r.id
770                  WHERE r.repositoryid = ?";
771         if (!empty($sort)) {
772             $sql .= " ORDER BY {$sort}";
773         }
775         $result = array();
776         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
777         foreach ($filerecords as $filerecord) {
778             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
779         }
780         return $result;
781     }
783     /**
784      * Returns all area files (optionally limited by itemid)
785      *
786      * @param int $contextid context ID
787      * @param string $component component
788      * @param string $filearea file area
789      * @param int $itemid item ID or all files if not specified
790      * @param string $sort A fragment of SQL to use for sorting
791      * @param bool $includedirs whether or not include directories
792      * @return stored_file[] array of stored_files indexed by pathanmehash
793      */
794     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
795         global $DB;
797         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
798         if ($itemid !== false) {
799             $itemidsql = ' AND f.itemid = :itemid ';
800             $conditions['itemid'] = $itemid;
801         } else {
802             $itemidsql = '';
803         }
805         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
806                   FROM {files} f
807              LEFT JOIN {files_reference} r
808                        ON f.referencefileid = r.id
809                  WHERE f.contextid = :contextid
810                        AND f.component = :component
811                        AND f.filearea = :filearea
812                        $itemidsql";
813         if (!empty($sort)) {
814             $sql .= " ORDER BY {$sort}";
815         }
817         $result = array();
818         $filerecords = $DB->get_records_sql($sql, $conditions);
819         foreach ($filerecords as $filerecord) {
820             if (!$includedirs and $filerecord->filename === '.') {
821                 continue;
822             }
823             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
824         }
825         return $result;
826     }
828     /**
829      * Returns array based tree structure of area files
830      *
831      * @param int $contextid context ID
832      * @param string $component component
833      * @param string $filearea file area
834      * @param int $itemid item ID
835      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
836      */
837     public function get_area_tree($contextid, $component, $filearea, $itemid) {
838         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
839         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
840         // first create directory structure
841         foreach ($files as $hash=>$dir) {
842             if (!$dir->is_directory()) {
843                 continue;
844             }
845             unset($files[$hash]);
846             if ($dir->get_filepath() === '/') {
847                 $result['dirfile'] = $dir;
848                 continue;
849             }
850             $parts = explode('/', trim($dir->get_filepath(),'/'));
851             $pointer =& $result;
852             foreach ($parts as $part) {
853                 if ($part === '') {
854                     continue;
855                 }
856                 if (!isset($pointer['subdirs'][$part])) {
857                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
858                 }
859                 $pointer =& $pointer['subdirs'][$part];
860             }
861             $pointer['dirfile'] = $dir;
862             unset($pointer);
863         }
864         foreach ($files as $hash=>$file) {
865             $parts = explode('/', trim($file->get_filepath(),'/'));
866             $pointer =& $result;
867             foreach ($parts as $part) {
868                 if ($part === '') {
869                     continue;
870                 }
871                 $pointer =& $pointer['subdirs'][$part];
872             }
873             $pointer['files'][$file->get_filename()] = $file;
874             unset($pointer);
875         }
876         $result = $this->sort_area_tree($result);
877         return $result;
878     }
880     /**
881      * Sorts the result of {@link file_storage::get_area_tree()}.
882      *
883      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
884      * @return array of sorted results
885      */
886     protected function sort_area_tree($tree) {
887         foreach ($tree as $key => &$value) {
888             if ($key == 'subdirs') {
889                 core_collator::ksort($value, core_collator::SORT_NATURAL);
890                 foreach ($value as $subdirname => &$subtree) {
891                     $subtree = $this->sort_area_tree($subtree);
892                 }
893             } else if ($key == 'files') {
894                 core_collator::ksort($value, core_collator::SORT_NATURAL);
895             }
896         }
897         return $tree;
898     }
900     /**
901      * Returns all files and optionally directories
902      *
903      * @param int $contextid context ID
904      * @param string $component component
905      * @param string $filearea file area
906      * @param int $itemid item ID
907      * @param int $filepath directory path
908      * @param bool $recursive include all subdirectories
909      * @param bool $includedirs include files and directories
910      * @param string $sort A fragment of SQL to use for sorting
911      * @return array of stored_files indexed by pathanmehash
912      */
913     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
914         global $DB;
916         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
917             return array();
918         }
920         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
922         if ($recursive) {
924             $dirs = $includedirs ? "" : "AND filename <> '.'";
925             $length = core_text::strlen($filepath);
927             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
928                       FROM {files} f
929                  LEFT JOIN {files_reference} r
930                            ON f.referencefileid = r.id
931                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
932                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
933                            AND f.id <> :dirid
934                            $dirs
935                            $orderby";
936             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
938             $files = array();
939             $dirs  = array();
940             $filerecords = $DB->get_records_sql($sql, $params);
941             foreach ($filerecords as $filerecord) {
942                 if ($filerecord->filename == '.') {
943                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
944                 } else {
945                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
946                 }
947             }
948             $result = array_merge($dirs, $files);
950         } else {
951             $result = array();
952             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
954             $length = core_text::strlen($filepath);
956             if ($includedirs) {
957                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
958                           FROM {files} f
959                      LEFT JOIN {files_reference} r
960                                ON f.referencefileid = r.id
961                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
962                                AND f.itemid = :itemid AND f.filename = '.'
963                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
964                                AND f.id <> :dirid
965                                $orderby";
966                 $reqlevel = substr_count($filepath, '/') + 1;
967                 $filerecords = $DB->get_records_sql($sql, $params);
968                 foreach ($filerecords as $filerecord) {
969                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
970                         continue;
971                     }
972                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
973                 }
974             }
976             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
977                       FROM {files} f
978                  LEFT JOIN {files_reference} r
979                            ON f.referencefileid = r.id
980                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
981                            AND f.filepath = :filepath AND f.filename <> '.'
982                            $orderby";
984             $filerecords = $DB->get_records_sql($sql, $params);
985             foreach ($filerecords as $filerecord) {
986                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
987             }
988         }
990         return $result;
991     }
993     /**
994      * Delete all area files (optionally limited by itemid).
995      *
996      * @param int $contextid context ID
997      * @param string $component component
998      * @param string $filearea file area or all areas in context if not specified
999      * @param int $itemid item ID or all files if not specified
1000      * @return bool success
1001      */
1002     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
1003         global $DB;
1005         $conditions = array('contextid'=>$contextid);
1006         if ($component !== false) {
1007             $conditions['component'] = $component;
1008         }
1009         if ($filearea !== false) {
1010             $conditions['filearea'] = $filearea;
1011         }
1012         if ($itemid !== false) {
1013             $conditions['itemid'] = $itemid;
1014         }
1016         $filerecords = $DB->get_records('files', $conditions);
1017         foreach ($filerecords as $filerecord) {
1018             $this->get_file_instance($filerecord)->delete();
1019         }
1021         return true; // BC only
1022     }
1024     /**
1025      * Delete all the files from certain areas where itemid is limited by an
1026      * arbitrary bit of SQL.
1027      *
1028      * @param int $contextid the id of the context the files belong to. Must be given.
1029      * @param string $component the owning component. Must be given.
1030      * @param string $filearea the file area name. Must be given.
1031      * @param string $itemidstest an SQL fragment that the itemid must match. Used
1032      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
1033      *      and may not used named parameters called contextid, component or filearea.
1034      * @param array $params any query params used by $itemidstest.
1035      */
1036     public function delete_area_files_select($contextid, $component,
1037             $filearea, $itemidstest, array $params = null) {
1038         global $DB;
1040         $where = "contextid = :contextid
1041                 AND component = :component
1042                 AND filearea = :filearea
1043                 AND itemid $itemidstest";
1044         $params['contextid'] = $contextid;
1045         $params['component'] = $component;
1046         $params['filearea'] = $filearea;
1048         $filerecords = $DB->get_recordset_select('files', $where, $params);
1049         foreach ($filerecords as $filerecord) {
1050             $this->get_file_instance($filerecord)->delete();
1051         }
1052         $filerecords->close();
1053     }
1055     /**
1056      * Delete all files associated with the given component.
1057      *
1058      * @param string $component the component owning the file
1059      */
1060     public function delete_component_files($component) {
1061         global $DB;
1063         $filerecords = $DB->get_recordset('files', array('component' => $component));
1064         foreach ($filerecords as $filerecord) {
1065             $this->get_file_instance($filerecord)->delete();
1066         }
1067         $filerecords->close();
1068     }
1070     /**
1071      * Move all the files in a file area from one context to another.
1072      *
1073      * @param int $oldcontextid the context the files are being moved from.
1074      * @param int $newcontextid the context the files are being moved to.
1075      * @param string $component the plugin that these files belong to.
1076      * @param string $filearea the name of the file area.
1077      * @param int $itemid file item ID
1078      * @return int the number of files moved, for information.
1079      */
1080     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
1081         // Note, this code is based on some code that Petr wrote in
1082         // forum_move_attachments in mod/forum/lib.php. I moved it here because
1083         // I needed it in the question code too.
1084         $count = 0;
1086         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
1087         foreach ($oldfiles as $oldfile) {
1088             $filerecord = new stdClass();
1089             $filerecord->contextid = $newcontextid;
1090             $this->create_file_from_storedfile($filerecord, $oldfile);
1091             $count += 1;
1092         }
1094         if ($count) {
1095             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
1096         }
1098         return $count;
1099     }
1101     /**
1102      * Recursively creates directory.
1103      *
1104      * @param int $contextid context ID
1105      * @param string $component component
1106      * @param string $filearea file area
1107      * @param int $itemid item ID
1108      * @param string $filepath file path
1109      * @param int $userid the user ID
1110      * @return bool success
1111      */
1112     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
1113         global $DB;
1115         // validate all parameters, we do not want any rubbish stored in database, right?
1116         if (!is_number($contextid) or $contextid < 1) {
1117             throw new file_exception('storedfileproblem', 'Invalid contextid');
1118         }
1120         $component = clean_param($component, PARAM_COMPONENT);
1121         if (empty($component)) {
1122             throw new file_exception('storedfileproblem', 'Invalid component');
1123         }
1125         $filearea = clean_param($filearea, PARAM_AREA);
1126         if (empty($filearea)) {
1127             throw new file_exception('storedfileproblem', 'Invalid filearea');
1128         }
1130         if (!is_number($itemid) or $itemid < 0) {
1131             throw new file_exception('storedfileproblem', 'Invalid itemid');
1132         }
1134         $filepath = clean_param($filepath, PARAM_PATH);
1135         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1136             // path must start and end with '/'
1137             throw new file_exception('storedfileproblem', 'Invalid file path');
1138         }
1140         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1142         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1143             return $dir_info;
1144         }
1146         static $contenthash = null;
1147         if (!$contenthash) {
1148             $this->add_string_to_pool('');
1149             $contenthash = sha1('');
1150         }
1152         $now = time();
1154         $dir_record = new stdClass();
1155         $dir_record->contextid = $contextid;
1156         $dir_record->component = $component;
1157         $dir_record->filearea  = $filearea;
1158         $dir_record->itemid    = $itemid;
1159         $dir_record->filepath  = $filepath;
1160         $dir_record->filename  = '.';
1161         $dir_record->contenthash  = $contenthash;
1162         $dir_record->filesize  = 0;
1164         $dir_record->timecreated  = $now;
1165         $dir_record->timemodified = $now;
1166         $dir_record->mimetype     = null;
1167         $dir_record->userid       = $userid;
1169         $dir_record->pathnamehash = $pathnamehash;
1171         $DB->insert_record('files', $dir_record);
1172         $dir_info = $this->get_file_by_hash($pathnamehash);
1174         if ($filepath !== '/') {
1175             //recurse to parent dirs
1176             $filepath = trim($filepath, '/');
1177             $filepath = explode('/', $filepath);
1178             array_pop($filepath);
1179             $filepath = implode('/', $filepath);
1180             $filepath = ($filepath === '') ? '/' : "/$filepath/";
1181             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1182         }
1184         return $dir_info;
1185     }
1187     /**
1188      * Add new local file based on existing local file.
1189      *
1190      * @param stdClass|array $filerecord object or array describing changes
1191      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1192      * @return stored_file instance of newly created file
1193      */
1194     public function create_file_from_storedfile($filerecord, $fileorid) {
1195         global $DB;
1197         if ($fileorid instanceof stored_file) {
1198             $fid = $fileorid->get_id();
1199         } else {
1200             $fid = $fileorid;
1201         }
1203         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1205         unset($filerecord['id']);
1206         unset($filerecord['filesize']);
1207         unset($filerecord['contenthash']);
1208         unset($filerecord['pathnamehash']);
1210         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1211                   FROM {files} f
1212              LEFT JOIN {files_reference} r
1213                        ON f.referencefileid = r.id
1214                  WHERE f.id = ?";
1216         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1217             throw new file_exception('storedfileproblem', 'File does not exist');
1218         }
1220         unset($newrecord->id);
1222         foreach ($filerecord as $key => $value) {
1223             // validate all parameters, we do not want any rubbish stored in database, right?
1224             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1225                 throw new file_exception('storedfileproblem', 'Invalid contextid');
1226             }
1228             if ($key == 'component') {
1229                 $value = clean_param($value, PARAM_COMPONENT);
1230                 if (empty($value)) {
1231                     throw new file_exception('storedfileproblem', 'Invalid component');
1232                 }
1233             }
1235             if ($key == 'filearea') {
1236                 $value = clean_param($value, PARAM_AREA);
1237                 if (empty($value)) {
1238                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1239                 }
1240             }
1242             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1243                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1244             }
1247             if ($key == 'filepath') {
1248                 $value = clean_param($value, PARAM_PATH);
1249                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1250                     // path must start and end with '/'
1251                     throw new file_exception('storedfileproblem', 'Invalid file path');
1252                 }
1253             }
1255             if ($key == 'filename') {
1256                 $value = clean_param($value, PARAM_FILE);
1257                 if ($value === '') {
1258                     // path must start and end with '/'
1259                     throw new file_exception('storedfileproblem', 'Invalid file name');
1260                 }
1261             }
1263             if ($key === 'timecreated' or $key === 'timemodified') {
1264                 if (!is_number($value)) {
1265                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1266                 }
1267                 if ($value < 0) {
1268                     //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)
1269                     $value = 0;
1270                 }
1271             }
1273             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1274                 $value = clean_param($value, PARAM_INT);
1275             }
1277             $newrecord->$key = $value;
1278         }
1280         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1282         if ($newrecord->filename === '.') {
1283             // special case - only this function supports directories ;-)
1284             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1285             // update the existing directory with the new data
1286             $newrecord->id = $directory->get_id();
1287             $DB->update_record('files', $newrecord);
1288             return $this->get_file_instance($newrecord);
1289         }
1291         // note: referencefileid is copied from the original file so that
1292         // creating a new file from an existing alias creates new alias implicitly.
1293         // here we just check the database consistency.
1294         if (!empty($newrecord->repositoryid)) {
1295             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1296                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1297             }
1298         }
1300         try {
1301             $newrecord->id = $DB->insert_record('files', $newrecord);
1302         } catch (dml_exception $e) {
1303             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1304                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1305         }
1308         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1310         return $this->get_file_instance($newrecord);
1311     }
1313     /**
1314      * Add new local file.
1315      *
1316      * @param stdClass|array $filerecord object or array describing file
1317      * @param string $url the URL to the file
1318      * @param array $options {@link download_file_content()} options
1319      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1320      * @return stored_file
1321      */
1322     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1324         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1325         $filerecord = (object)$filerecord; // We support arrays too.
1327         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1328         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1329         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1330         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1331         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1332         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1333         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1335         if (!isset($filerecord->filename)) {
1336             $parts = explode('/', $url);
1337             $filename = array_pop($parts);
1338             $filerecord->filename = clean_param($filename, PARAM_FILE);
1339         }
1340         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1341         $filerecord->source = clean_param($source, PARAM_URL);
1343         if ($usetempfile) {
1344             check_dir_exists($this->tempdir);
1345             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1346             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1347             if ($content === false) {
1348                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1349             }
1350             try {
1351                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1352                 @unlink($tmpfile);
1353                 return $newfile;
1354             } catch (Exception $e) {
1355                 @unlink($tmpfile);
1356                 throw $e;
1357             }
1359         } else {
1360             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1361             if ($content === false) {
1362                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1363             }
1364             return $this->create_file_from_string($filerecord, $content);
1365         }
1366     }
1368     /**
1369      * Add new local file.
1370      *
1371      * @param stdClass|array $filerecord object or array describing file
1372      * @param string $pathname path to file or content of file
1373      * @return stored_file
1374      */
1375     public function create_file_from_pathname($filerecord, $pathname) {
1376         global $DB;
1378         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1379         $filerecord = (object)$filerecord; // We support arrays too.
1381         // validate all parameters, we do not want any rubbish stored in database, right?
1382         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1383             throw new file_exception('storedfileproblem', 'Invalid contextid');
1384         }
1386         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1387         if (empty($filerecord->component)) {
1388             throw new file_exception('storedfileproblem', 'Invalid component');
1389         }
1391         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1392         if (empty($filerecord->filearea)) {
1393             throw new file_exception('storedfileproblem', 'Invalid filearea');
1394         }
1396         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1397             throw new file_exception('storedfileproblem', 'Invalid itemid');
1398         }
1400         if (!empty($filerecord->sortorder)) {
1401             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1402                 $filerecord->sortorder = 0;
1403             }
1404         } else {
1405             $filerecord->sortorder = 0;
1406         }
1408         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1409         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1410             // path must start and end with '/'
1411             throw new file_exception('storedfileproblem', 'Invalid file path');
1412         }
1414         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1415         if ($filerecord->filename === '') {
1416             // filename must not be empty
1417             throw new file_exception('storedfileproblem', 'Invalid file name');
1418         }
1420         $now = time();
1421         if (isset($filerecord->timecreated)) {
1422             if (!is_number($filerecord->timecreated)) {
1423                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1424             }
1425             if ($filerecord->timecreated < 0) {
1426                 //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)
1427                 $filerecord->timecreated = 0;
1428             }
1429         } else {
1430             $filerecord->timecreated = $now;
1431         }
1433         if (isset($filerecord->timemodified)) {
1434             if (!is_number($filerecord->timemodified)) {
1435                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1436             }
1437             if ($filerecord->timemodified < 0) {
1438                 //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)
1439                 $filerecord->timemodified = 0;
1440             }
1441         } else {
1442             $filerecord->timemodified = $now;
1443         }
1445         $newrecord = new stdClass();
1447         $newrecord->contextid = $filerecord->contextid;
1448         $newrecord->component = $filerecord->component;
1449         $newrecord->filearea  = $filerecord->filearea;
1450         $newrecord->itemid    = $filerecord->itemid;
1451         $newrecord->filepath  = $filerecord->filepath;
1452         $newrecord->filename  = $filerecord->filename;
1454         $newrecord->timecreated  = $filerecord->timecreated;
1455         $newrecord->timemodified = $filerecord->timemodified;
1456         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1457         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1458         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1459         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1460         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1461         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1462         $newrecord->sortorder    = $filerecord->sortorder;
1464         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1466         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1468         try {
1469             $newrecord->id = $DB->insert_record('files', $newrecord);
1470         } catch (dml_exception $e) {
1471             if ($newfile) {
1472                 $this->deleted_file_cleanup($newrecord->contenthash);
1473             }
1474             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1475                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1476         }
1478         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1480         return $this->get_file_instance($newrecord);
1481     }
1483     /**
1484      * Add new local file.
1485      *
1486      * @param stdClass|array $filerecord object or array describing file
1487      * @param string $content content of file
1488      * @return stored_file
1489      */
1490     public function create_file_from_string($filerecord, $content) {
1491         global $DB;
1493         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1494         $filerecord = (object)$filerecord; // We support arrays too.
1496         // validate all parameters, we do not want any rubbish stored in database, right?
1497         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1498             throw new file_exception('storedfileproblem', 'Invalid contextid');
1499         }
1501         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1502         if (empty($filerecord->component)) {
1503             throw new file_exception('storedfileproblem', 'Invalid component');
1504         }
1506         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1507         if (empty($filerecord->filearea)) {
1508             throw new file_exception('storedfileproblem', 'Invalid filearea');
1509         }
1511         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1512             throw new file_exception('storedfileproblem', 'Invalid itemid');
1513         }
1515         if (!empty($filerecord->sortorder)) {
1516             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1517                 $filerecord->sortorder = 0;
1518             }
1519         } else {
1520             $filerecord->sortorder = 0;
1521         }
1523         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1524         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1525             // path must start and end with '/'
1526             throw new file_exception('storedfileproblem', 'Invalid file path');
1527         }
1529         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1530         if ($filerecord->filename === '') {
1531             // path must start and end with '/'
1532             throw new file_exception('storedfileproblem', 'Invalid file name');
1533         }
1535         $now = time();
1536         if (isset($filerecord->timecreated)) {
1537             if (!is_number($filerecord->timecreated)) {
1538                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1539             }
1540             if ($filerecord->timecreated < 0) {
1541                 //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)
1542                 $filerecord->timecreated = 0;
1543             }
1544         } else {
1545             $filerecord->timecreated = $now;
1546         }
1548         if (isset($filerecord->timemodified)) {
1549             if (!is_number($filerecord->timemodified)) {
1550                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1551             }
1552             if ($filerecord->timemodified < 0) {
1553                 //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)
1554                 $filerecord->timemodified = 0;
1555             }
1556         } else {
1557             $filerecord->timemodified = $now;
1558         }
1560         $newrecord = new stdClass();
1562         $newrecord->contextid = $filerecord->contextid;
1563         $newrecord->component = $filerecord->component;
1564         $newrecord->filearea  = $filerecord->filearea;
1565         $newrecord->itemid    = $filerecord->itemid;
1566         $newrecord->filepath  = $filerecord->filepath;
1567         $newrecord->filename  = $filerecord->filename;
1569         $newrecord->timecreated  = $filerecord->timecreated;
1570         $newrecord->timemodified = $filerecord->timemodified;
1571         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1572         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1573         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1574         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1575         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1576         $newrecord->sortorder    = $filerecord->sortorder;
1578         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1579         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1580         // get mimetype by magic bytes
1581         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1583         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1585         try {
1586             $newrecord->id = $DB->insert_record('files', $newrecord);
1587         } catch (dml_exception $e) {
1588             if ($newfile) {
1589                 $this->deleted_file_cleanup($newrecord->contenthash);
1590             }
1591             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1592                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1593         }
1595         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1597         return $this->get_file_instance($newrecord);
1598     }
1600     /**
1601      * Create a new alias/shortcut file from file reference information
1602      *
1603      * @param stdClass|array $filerecord object or array describing the new file
1604      * @param int $repositoryid the id of the repository that provides the original file
1605      * @param string $reference the information required by the repository to locate the original file
1606      * @param array $options options for creating the new file
1607      * @return stored_file
1608      */
1609     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1610         global $DB;
1612         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1613         $filerecord = (object)$filerecord; // We support arrays too.
1615         // validate all parameters, we do not want any rubbish stored in database, right?
1616         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1617             throw new file_exception('storedfileproblem', 'Invalid contextid');
1618         }
1620         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1621         if (empty($filerecord->component)) {
1622             throw new file_exception('storedfileproblem', 'Invalid component');
1623         }
1625         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1626         if (empty($filerecord->filearea)) {
1627             throw new file_exception('storedfileproblem', 'Invalid filearea');
1628         }
1630         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1631             throw new file_exception('storedfileproblem', 'Invalid itemid');
1632         }
1634         if (!empty($filerecord->sortorder)) {
1635             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1636                 $filerecord->sortorder = 0;
1637             }
1638         } else {
1639             $filerecord->sortorder = 0;
1640         }
1642         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1643         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1644         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1645         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1646         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1647         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1648         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1649         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1650             // Path must start and end with '/'.
1651             throw new file_exception('storedfileproblem', 'Invalid file path');
1652         }
1654         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1655         if ($filerecord->filename === '') {
1656             // Path must start and end with '/'.
1657             throw new file_exception('storedfileproblem', 'Invalid file name');
1658         }
1660         $now = time();
1661         if (isset($filerecord->timecreated)) {
1662             if (!is_number($filerecord->timecreated)) {
1663                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1664             }
1665             if ($filerecord->timecreated < 0) {
1666                 // 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)
1667                 $filerecord->timecreated = 0;
1668             }
1669         } else {
1670             $filerecord->timecreated = $now;
1671         }
1673         if (isset($filerecord->timemodified)) {
1674             if (!is_number($filerecord->timemodified)) {
1675                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1676             }
1677             if ($filerecord->timemodified < 0) {
1678                 // 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)
1679                 $filerecord->timemodified = 0;
1680             }
1681         } else {
1682             $filerecord->timemodified = $now;
1683         }
1685         $transaction = $DB->start_delegated_transaction();
1687         try {
1688             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1689         } catch (Exception $e) {
1690             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1691         }
1693         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1694             // there was specified the contenthash for a file already stored in moodle filepool
1695             if (empty($filerecord->filesize)) {
1696                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1697                 $filerecord->filesize = filesize($filepathname);
1698             } else {
1699                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1700             }
1701         } else {
1702             // atempt to get the result of last synchronisation for this reference
1703             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1704                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1705             if ($lastcontent) {
1706                 $filerecord->contenthash = $lastcontent->contenthash;
1707                 $filerecord->filesize = $lastcontent->filesize;
1708             } else {
1709                 // External file doesn't have content in moodle.
1710                 // So we create an empty file for it.
1711                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1712             }
1713         }
1715         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1717         try {
1718             $filerecord->id = $DB->insert_record('files', $filerecord);
1719         } catch (dml_exception $e) {
1720             if (!empty($newfile)) {
1721                 $this->deleted_file_cleanup($filerecord->contenthash);
1722             }
1723             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1724                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1725         }
1727         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1729         $transaction->allow_commit();
1731         // this will retrieve all reference information from DB as well
1732         return $this->get_file_by_id($filerecord->id);
1733     }
1735     /**
1736      * Creates new image file from existing.
1737      *
1738      * @param stdClass|array $filerecord object or array describing new file
1739      * @param int|stored_file $fid file id or stored file object
1740      * @param int $newwidth in pixels
1741      * @param int $newheight in pixels
1742      * @param bool $keepaspectratio whether or not keep aspect ratio
1743      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1744      * @return stored_file
1745      */
1746     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1747         if (!function_exists('imagecreatefromstring')) {
1748             //Most likely the GD php extension isn't installed
1749             //image conversion cannot succeed
1750             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1751         }
1753         if ($fid instanceof stored_file) {
1754             $fid = $fid->get_id();
1755         }
1757         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1759         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1760             throw new file_exception('storedfileproblem', 'File does not exist');
1761         }
1763         if (!$imageinfo = $file->get_imageinfo()) {
1764             throw new file_exception('storedfileproblem', 'File is not an image');
1765         }
1767         if (!isset($filerecord['filename'])) {
1768             $filerecord['filename'] = $file->get_filename();
1769         }
1771         if (!isset($filerecord['mimetype'])) {
1772             $filerecord['mimetype'] = $imageinfo['mimetype'];
1773         }
1775         $width    = $imageinfo['width'];
1776         $height   = $imageinfo['height'];
1778         if ($keepaspectratio) {
1779             if (0 >= $newwidth and 0 >= $newheight) {
1780                 // no sizes specified
1781                 $newwidth  = $width;
1782                 $newheight = $height;
1784             } else if (0 < $newwidth and 0 < $newheight) {
1785                 $xheight = ($newwidth*($height/$width));
1786                 if ($xheight < $newheight) {
1787                     $newheight = (int)$xheight;
1788                 } else {
1789                     $newwidth = (int)($newheight*($width/$height));
1790                 }
1792             } else if (0 < $newwidth) {
1793                 $newheight = (int)($newwidth*($height/$width));
1795             } else { //0 < $newheight
1796                 $newwidth = (int)($newheight*($width/$height));
1797             }
1799         } else {
1800             if (0 >= $newwidth) {
1801                 $newwidth = $width;
1802             }
1803             if (0 >= $newheight) {
1804                 $newheight = $height;
1805             }
1806         }
1808         // The original image.
1809         $img = imagecreatefromstring($file->get_content());
1811         // A new true color image where we will copy our original image.
1812         $newimg = imagecreatetruecolor($newwidth, $newheight);
1814         // Determine if the file supports transparency.
1815         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1817         // Maintain transparency.
1818         if ($hasalpha) {
1819             imagealphablending($newimg, true);
1821             // Get the current transparent index for the original image.
1822             $colour = imagecolortransparent($img);
1823             if ($colour == -1) {
1824                 // Set a transparent colour index if there's none.
1825                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1826                 // Save full alpha channel.
1827                 imagesavealpha($newimg, true);
1828             }
1829             imagecolortransparent($newimg, $colour);
1830             imagefill($newimg, 0, 0, $colour);
1831         }
1833         // Process the image to be output.
1834         if ($height != $newheight or $width != $newwidth) {
1835             // Resample if the dimensions differ from the original.
1836             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1837                 // weird
1838                 throw new file_exception('storedfileproblem', 'Can not resize image');
1839             }
1840             imagedestroy($img);
1841             $img = $newimg;
1843         } else if ($hasalpha) {
1844             // Just copy to the new image with the alpha channel.
1845             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1846                 // Weird.
1847                 throw new file_exception('storedfileproblem', 'Can not copy image');
1848             }
1849             imagedestroy($img);
1850             $img = $newimg;
1852         } else {
1853             // No particular processing needed for the original image.
1854             imagedestroy($newimg);
1855         }
1857         ob_start();
1858         switch ($filerecord['mimetype']) {
1859             case 'image/gif':
1860                 imagegif($img);
1861                 break;
1863             case 'image/jpeg':
1864                 if (is_null($quality)) {
1865                     imagejpeg($img);
1866                 } else {
1867                     imagejpeg($img, NULL, $quality);
1868                 }
1869                 break;
1871             case 'image/png':
1872                 $quality = (int)$quality;
1874                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1875                 // the latter of which can go to 100, we need to make sure that quality here is
1876                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1877                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1878                 imagepng($img, NULL, $quality, NULL);
1879                 break;
1881             default:
1882                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1883         }
1885         $content = ob_get_contents();
1886         ob_end_clean();
1887         imagedestroy($img);
1889         if (!$content) {
1890             throw new file_exception('storedfileproblem', 'Can not convert image');
1891         }
1893         return $this->create_file_from_string($filerecord, $content);
1894     }
1896     /**
1897      * Add file content to sha1 pool.
1898      *
1899      * @param string $pathname path to file
1900      * @param string $contenthash sha1 hash of content if known (performance only)
1901      * @return array (contenthash, filesize, newfile)
1902      */
1903     public function add_file_to_pool($pathname, $contenthash = NULL) {
1904         global $CFG;
1906         if (!is_readable($pathname)) {
1907             throw new file_exception('storedfilecannotread', '', $pathname);
1908         }
1910         $filesize = filesize($pathname);
1911         if ($filesize === false) {
1912             throw new file_exception('storedfilecannotread', '', $pathname);
1913         }
1915         if (is_null($contenthash)) {
1916             $contenthash = sha1_file($pathname);
1917         } else if ($CFG->debugdeveloper) {
1918             $filehash = sha1_file($pathname);
1919             if ($filehash === false) {
1920                 throw new file_exception('storedfilecannotread', '', $pathname);
1921             }
1922             if ($filehash !== $contenthash) {
1923                 // Hopefully this never happens, if yes we need to fix calling code.
1924                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1925                 $contenthash = $filehash;
1926             }
1927         }
1928         if ($contenthash === false) {
1929             throw new file_exception('storedfilecannotread', '', $pathname);
1930         }
1932         if ($filesize > 0 and $contenthash === sha1('')) {
1933             // Did the file change or is sha1_file() borked for this file?
1934             clearstatcache();
1935             $contenthash = sha1_file($pathname);
1936             $filesize = filesize($pathname);
1938             if ($contenthash === false or $filesize === false) {
1939                 throw new file_exception('storedfilecannotread', '', $pathname);
1940             }
1941             if ($filesize > 0 and $contenthash === sha1('')) {
1942                 // This is very weird...
1943                 throw new file_exception('storedfilecannotread', '', $pathname);
1944             }
1945         }
1947         $hashpath = $this->path_from_hash($contenthash);
1948         $hashfile = "$hashpath/$contenthash";
1950         $newfile = true;
1952         if (file_exists($hashfile)) {
1953             if (filesize($hashfile) === $filesize) {
1954                 return array($contenthash, $filesize, false);
1955             }
1956             if (sha1_file($hashfile) === $contenthash) {
1957                 // Jackpot! We have a sha1 collision.
1958                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1959                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1960                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1961                 throw new file_pool_content_exception($contenthash);
1962             }
1963             debugging("Replacing invalid content file $contenthash");
1964             unlink($hashfile);
1965             $newfile = false;
1966         }
1968         if (!is_dir($hashpath)) {
1969             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1970                 // Permission trouble.
1971                 throw new file_exception('storedfilecannotcreatefiledirs');
1972             }
1973         }
1975         // Let's try to prevent some race conditions.
1977         $prev = ignore_user_abort(true);
1978         @unlink($hashfile.'.tmp');
1979         if (!copy($pathname, $hashfile.'.tmp')) {
1980             // Borked permissions or out of disk space.
1981             ignore_user_abort($prev);
1982             throw new file_exception('storedfilecannotcreatefile');
1983         }
1984         if (filesize($hashfile.'.tmp') !== $filesize) {
1985             // This should not happen.
1986             unlink($hashfile.'.tmp');
1987             ignore_user_abort($prev);
1988             throw new file_exception('storedfilecannotcreatefile');
1989         }
1990         rename($hashfile.'.tmp', $hashfile);
1991         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1992         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1993         ignore_user_abort($prev);
1995         return array($contenthash, $filesize, $newfile);
1996     }
1998     /**
1999      * Add string content to sha1 pool.
2000      *
2001      * @param string $content file content - binary string
2002      * @return array (contenthash, filesize, newfile)
2003      */
2004     public function add_string_to_pool($content) {
2005         global $CFG;
2007         $contenthash = sha1($content);
2008         $filesize = strlen($content); // binary length
2010         $hashpath = $this->path_from_hash($contenthash);
2011         $hashfile = "$hashpath/$contenthash";
2013         $newfile = true;
2015         if (file_exists($hashfile)) {
2016             if (filesize($hashfile) === $filesize) {
2017                 return array($contenthash, $filesize, false);
2018             }
2019             if (sha1_file($hashfile) === $contenthash) {
2020                 // Jackpot! We have a sha1 collision.
2021                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
2022                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
2023                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
2024                 throw new file_pool_content_exception($contenthash);
2025             }
2026             debugging("Replacing invalid content file $contenthash");
2027             unlink($hashfile);
2028             $newfile = false;
2029         }
2031         if (!is_dir($hashpath)) {
2032             if (!mkdir($hashpath, $this->dirpermissions, true)) {
2033                 // Permission trouble.
2034                 throw new file_exception('storedfilecannotcreatefiledirs');
2035             }
2036         }
2038         // Hopefully this works around most potential race conditions.
2040         $prev = ignore_user_abort(true);
2042         if (!empty($CFG->preventfilelocking)) {
2043             $newsize = file_put_contents($hashfile.'.tmp', $content);
2044         } else {
2045             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
2046         }
2048         if ($newsize === false) {
2049             // Borked permissions most likely.
2050             ignore_user_abort($prev);
2051             throw new file_exception('storedfilecannotcreatefile');
2052         }
2053         if (filesize($hashfile.'.tmp') !== $filesize) {
2054             // Out of disk space?
2055             unlink($hashfile.'.tmp');
2056             ignore_user_abort($prev);
2057             throw new file_exception('storedfilecannotcreatefile');
2058         }
2059         rename($hashfile.'.tmp', $hashfile);
2060         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
2061         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
2062         ignore_user_abort($prev);
2064         return array($contenthash, $filesize, $newfile);
2065     }
2067     /**
2068      * Serve file content using X-Sendfile header.
2069      * Please make sure that all headers are already sent
2070      * and the all access control checks passed.
2071      *
2072      * @param string $contenthash sah1 hash of the file content to be served
2073      * @return bool success
2074      */
2075     public function xsendfile($contenthash) {
2076         global $CFG;
2077         require_once("$CFG->libdir/xsendfilelib.php");
2079         $hashpath = $this->path_from_hash($contenthash);
2080         return xsendfile("$hashpath/$contenthash");
2081     }
2083     /**
2084      * Content exists
2085      *
2086      * @param string $contenthash
2087      * @return bool
2088      */
2089     public function content_exists($contenthash) {
2090         $dir = $this->path_from_hash($contenthash);
2091         $filepath = $dir . '/' . $contenthash;
2092         return file_exists($filepath);
2093     }
2095     /**
2096      * Return path to file with given hash.
2097      *
2098      * NOTE: must not be public, files in pool must not be modified
2099      *
2100      * @param string $contenthash content hash
2101      * @return string expected file location
2102      */
2103     protected function path_from_hash($contenthash) {
2104         $l1 = $contenthash[0].$contenthash[1];
2105         $l2 = $contenthash[2].$contenthash[3];
2106         return "$this->filedir/$l1/$l2";
2107     }
2109     /**
2110      * Return path to file with given hash.
2111      *
2112      * NOTE: must not be public, files in pool must not be modified
2113      *
2114      * @param string $contenthash content hash
2115      * @return string expected file location
2116      */
2117     protected function trash_path_from_hash($contenthash) {
2118         $l1 = $contenthash[0].$contenthash[1];
2119         $l2 = $contenthash[2].$contenthash[3];
2120         return "$this->trashdir/$l1/$l2";
2121     }
2123     /**
2124      * Tries to recover missing content of file from trash.
2125      *
2126      * @param stored_file $file stored_file instance
2127      * @return bool success
2128      */
2129     public function try_content_recovery($file) {
2130         $contenthash = $file->get_contenthash();
2131         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
2132         if (!is_readable($trashfile)) {
2133             if (!is_readable($this->trashdir.'/'.$contenthash)) {
2134                 return false;
2135             }
2136             // nice, at least alternative trash file in trash root exists
2137             $trashfile = $this->trashdir.'/'.$contenthash;
2138         }
2139         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
2140             //weird, better fail early
2141             return false;
2142         }
2143         $contentdir  = $this->path_from_hash($contenthash);
2144         $contentfile = $contentdir.'/'.$contenthash;
2145         if (file_exists($contentfile)) {
2146             //strange, no need to recover anything
2147             return true;
2148         }
2149         if (!is_dir($contentdir)) {
2150             if (!mkdir($contentdir, $this->dirpermissions, true)) {
2151                 return false;
2152             }
2153         }
2154         return rename($trashfile, $contentfile);
2155     }
2157     /**
2158      * Marks pool file as candidate for deleting.
2159      *
2160      * DO NOT call directly - reserved for core!!
2161      *
2162      * @param string $contenthash
2163      */
2164     public function deleted_file_cleanup($contenthash) {
2165         global $DB;
2167         if ($contenthash === sha1('')) {
2168             // No need to delete empty content file with sha1('') content hash.
2169             return;
2170         }
2172         //Note: this section is critical - in theory file could be reused at the same
2173         //      time, if this happens we can still recover the file from trash
2174         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
2175             // file content is still used
2176             return;
2177         }
2178         //move content file to trash
2179         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
2180         if (!file_exists($contentfile)) {
2181             //weird, but no problem
2182             return;
2183         }
2184         $trashpath = $this->trash_path_from_hash($contenthash);
2185         $trashfile = $trashpath.'/'.$contenthash;
2186         if (file_exists($trashfile)) {
2187             // we already have this content in trash, no need to move it there
2188             unlink($contentfile);
2189             return;
2190         }
2191         if (!is_dir($trashpath)) {
2192             mkdir($trashpath, $this->dirpermissions, true);
2193         }
2194         rename($contentfile, $trashfile);
2195         chmod($trashfile, $this->filepermissions); // fix permissions if needed
2196     }
2198     /**
2199      * When user referring to a moodle file, we build the reference field
2200      *
2201      * @param array $params
2202      * @return string
2203      */
2204     public static function pack_reference($params) {
2205         $params = (array)$params;
2206         $reference = array();
2207         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
2208         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
2209         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
2210         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
2211         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
2212         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
2213         return base64_encode(serialize($reference));
2214     }
2216     /**
2217      * Unpack reference field
2218      *
2219      * @param string $str
2220      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
2221      * @throws file_reference_exception if the $str does not have the expected format
2222      * @return array
2223      */
2224     public static function unpack_reference($str, $cleanparams = false) {
2225         $decoded = base64_decode($str, true);
2226         if ($decoded === false) {
2227             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
2228         }
2229         $params = @unserialize($decoded); // hide E_NOTICE
2230         if ($params === false) {
2231             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
2232         }
2233         if (is_array($params) && $cleanparams) {
2234             $params = array(
2235                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
2236                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
2237                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
2238                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
2239                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
2240                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
2241             );
2242         }
2243         return $params;
2244     }
2246     /**
2247      * Search through the server files.
2248      *
2249      * The query parameter will be used in conjuction with the SQL directive
2250      * LIKE, so include '%' in it if you need to. This search will always ignore
2251      * user files and directories. Note that the search is case insensitive.
2252      *
2253      * This query can quickly become inefficient so use it sparignly.
2254      *
2255      * @param  string  $query The string used with SQL LIKE.
2256      * @param  integer $from  The offset to start the search at.
2257      * @param  integer $limit The maximum number of results.
2258      * @param  boolean $count When true this methods returns the number of results availabe,
2259      *                        disregarding the parameters $from and $limit.
2260      * @return int|array      Integer when count, otherwise array of stored_file objects.
2261      */
2262     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
2263         global $DB;
2264         $params = array(
2265             'contextlevel' => CONTEXT_USER,
2266             'directory' => '.',
2267             'query' => $query
2268         );
2270         if ($count) {
2271             $select = 'COUNT(1)';
2272         } else {
2273             $select = self::instance_sql_fields('f', 'r');
2274         }
2275         $like = $DB->sql_like('f.filename', ':query', false);
2277         $sql = "SELECT $select
2278                   FROM {files} f
2279              LEFT JOIN {files_reference} r
2280                     ON f.referencefileid = r.id
2281                   JOIN {context} c
2282                     ON f.contextid = c.id
2283                  WHERE c.contextlevel <> :contextlevel
2284                    AND f.filename <> :directory
2285                    AND " . $like . "";
2287         if ($count) {
2288             return $DB->count_records_sql($sql, $params);
2289         }
2291         $sql .= " ORDER BY f.filename";
2293         $result = array();
2294         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2295         foreach ($filerecords as $filerecord) {
2296             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2297         }
2298         $filerecords->close();
2300         return $result;
2301     }
2303     /**
2304      * Returns all aliases that refer to some stored_file via the given reference
2305      *
2306      * All repositories that provide access to a stored_file are expected to use
2307      * {@link self::pack_reference()}. This method can't be used if the given reference
2308      * does not use this format or if you are looking for references to an external file
2309      * (for example it can't be used to search for all aliases that refer to a given
2310      * Dropbox or Box.net file).
2311      *
2312      * Aliases in user draft areas are excluded from the returned list.
2313      *
2314      * @param string $reference identification of the referenced file
2315      * @return array of stored_file indexed by its pathnamehash
2316      */
2317     public function search_references($reference) {
2318         global $DB;
2320         if (is_null($reference)) {
2321             throw new coding_exception('NULL is not a valid reference to an external file');
2322         }
2324         // Give {@link self::unpack_reference()} a chance to throw exception if the
2325         // reference is not in a valid format.
2326         self::unpack_reference($reference);
2328         $referencehash = sha1($reference);
2330         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2331                   FROM {files} f
2332                   JOIN {files_reference} r ON f.referencefileid = r.id
2333                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2334                  WHERE r.referencehash = ?
2335                        AND (f.component <> ? OR f.filearea <> ?)";
2337         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2338         $files = array();
2339         foreach ($rs as $filerecord) {
2340             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2341         }
2343         return $files;
2344     }
2346     /**
2347      * Returns the number of aliases that refer to some stored_file via the given reference
2348      *
2349      * All repositories that provide access to a stored_file are expected to use
2350      * {@link self::pack_reference()}. This method can't be used if the given reference
2351      * does not use this format or if you are looking for references to an external file
2352      * (for example it can't be used to count aliases that refer to a given Dropbox or
2353      * Box.net file).
2354      *
2355      * Aliases in user draft areas are not counted.
2356      *
2357      * @param string $reference identification of the referenced file
2358      * @return int
2359      */
2360     public function search_references_count($reference) {
2361         global $DB;
2363         if (is_null($reference)) {
2364             throw new coding_exception('NULL is not a valid reference to an external file');
2365         }
2367         // Give {@link self::unpack_reference()} a chance to throw exception if the
2368         // reference is not in a valid format.
2369         self::unpack_reference($reference);
2371         $referencehash = sha1($reference);
2373         $sql = "SELECT COUNT(f.id)
2374                   FROM {files} f
2375                   JOIN {files_reference} r ON f.referencefileid = r.id
2376                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2377                  WHERE r.referencehash = ?
2378                        AND (f.component <> ? OR f.filearea <> ?)";
2380         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2381     }
2383     /**
2384      * Returns all aliases that link to the given stored_file
2385      *
2386      * Aliases in user draft areas are excluded from the returned list.
2387      *
2388      * @param stored_file $storedfile
2389      * @return array of stored_file
2390      */
2391     public function get_references_by_storedfile(stored_file $storedfile) {
2392         global $DB;
2394         $params = array();
2395         $params['contextid'] = $storedfile->get_contextid();
2396         $params['component'] = $storedfile->get_component();
2397         $params['filearea']  = $storedfile->get_filearea();
2398         $params['itemid']    = $storedfile->get_itemid();
2399         $params['filename']  = $storedfile->get_filename();
2400         $params['filepath']  = $storedfile->get_filepath();
2402         return $this->search_references(self::pack_reference($params));
2403     }
2405     /**
2406      * Returns the number of aliases that link to the given stored_file
2407      *
2408      * Aliases in user draft areas are not counted.
2409      *
2410      * @param stored_file $storedfile
2411      * @return int
2412      */
2413     public function get_references_count_by_storedfile(stored_file $storedfile) {
2414         global $DB;
2416         $params = array();
2417         $params['contextid'] = $storedfile->get_contextid();
2418         $params['component'] = $storedfile->get_component();
2419         $params['filearea']  = $storedfile->get_filearea();
2420         $params['itemid']    = $storedfile->get_itemid();
2421         $params['filename']  = $storedfile->get_filename();
2422         $params['filepath']  = $storedfile->get_filepath();
2424         return $this->search_references_count(self::pack_reference($params));
2425     }
2427     /**
2428      * Updates all files that are referencing this file with the new contenthash
2429      * and filesize
2430      *
2431      * @param stored_file $storedfile
2432      */
2433     public function update_references_to_storedfile(stored_file $storedfile) {
2434         global $CFG, $DB;
2435         $params = array();
2436         $params['contextid'] = $storedfile->get_contextid();
2437         $params['component'] = $storedfile->get_component();
2438         $params['filearea']  = $storedfile->get_filearea();
2439         $params['itemid']    = $storedfile->get_itemid();
2440         $params['filename']  = $storedfile->get_filename();
2441         $params['filepath']  = $storedfile->get_filepath();
2442         $reference = self::pack_reference($params);
2443         $referencehash = sha1($reference);
2445         $sql = "SELECT repositoryid, id FROM {files_reference}
2446                  WHERE referencehash = ?";
2447         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2449         $now = time();
2450         foreach ($rs as $record) {
2451             $this->update_references($record->id, $now, null,
2452                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2453         }
2454         $rs->close();
2455     }
2457     /**
2458      * Convert file alias to local file
2459      *
2460      * @throws moodle_exception if file could not be downloaded
2461      *
2462      * @param stored_file $storedfile a stored_file instances
2463      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2464      * @return stored_file stored_file
2465      */
2466     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2467         global $CFG;
2468         $storedfile->import_external_file_contents($maxbytes);
2469         $storedfile->delete_reference();
2470         return $storedfile;
2471     }
2473     /**
2474      * Return mimetype by given file pathname
2475      *
2476      * If file has a known extension, we return the mimetype based on extension.
2477      * Otherwise (when possible) we try to get the mimetype from file contents.
2478      *
2479      * @param string $pathname full path to the file
2480      * @param string $filename correct file name with extension, if omitted will be taken from $path
2481      * @return string
2482      */
2483     public static function mimetype($pathname, $filename = null) {
2484         if (empty($filename)) {
2485             $filename = $pathname;
2486         }
2487         $type = mimeinfo('type', $filename);
2488         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2489             $finfo = new finfo(FILEINFO_MIME_TYPE);
2490             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2491         }
2492         return $type;
2493     }
2495     /**
2496      * Cron cleanup job.
2497      */
2498     public function cron() {
2499         global $CFG, $DB;
2500         require_once($CFG->libdir.'/cronlib.php');
2502         // find out all stale draft areas (older than 4 days) and purge them
2503         // those are identified by time stamp of the /. root dir
2504         mtrace('Deleting old draft files... ', '');
2505         cron_trace_time_and_memory();
2506         $old = time() - 60*60*24*4;
2507         $sql = "SELECT *
2508                   FROM {files}
2509                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2510                        AND timecreated < :old";
2511         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2512         foreach ($rs as $dir) {
2513             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2514         }
2515         $rs->close();
2516         mtrace('done.');
2518         // remove orphaned preview files (that is files in the core preview filearea without
2519         // the existing original file)
2520         mtrace('Deleting orphaned preview files... ', '');
2521         cron_trace_time_and_memory();
2522         $sql = "SELECT p.*
2523                   FROM {files} p
2524              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2525                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2526                        AND o.id IS NULL";
2527         $syscontext = context_system::instance();
2528         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2529         foreach ($rs as $orphan) {
2530             $file = $this->get_file_instance($orphan);
2531             if (!$file->is_directory()) {
2532                 $file->delete();
2533             }
2534         }
2535         $rs->close();
2536         mtrace('done.');
2538         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2539         // the existing original file).
2540         mtrace('Deleting orphaned document conversion files... ', '');
2541         cron_trace_time_and_memory();
2542         $sql = "SELECT p.*
2543                   FROM {files} p
2544              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2545                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
2546                        AND o.id IS NULL";
2547         $syscontext = context_system::instance();
2548         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2549         foreach ($rs as $orphan) {
2550             $file = $this->get_file_instance($orphan);
2551             if (!$file->is_directory()) {
2552                 $file->delete();
2553             }
2554         }
2555         $rs->close();
2556         mtrace('done.');
2558         // remove trash pool files once a day
2559         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2560         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2561             require_once($CFG->libdir.'/filelib.php');
2562             // Delete files that are associated with a context that no longer exists.
2563             mtrace('Cleaning up files from deleted contexts... ', '');
2564             cron_trace_time_and_memory();
2565             $sql = "SELECT DISTINCT f.contextid
2566                     FROM {files} f
2567                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2568                     WHERE c.id IS NULL";
2569             $rs = $DB->get_recordset_sql($sql);
2570             if ($rs->valid()) {
2571                 $fs = get_file_storage();
2572                 foreach ($rs as $ctx) {
2573                     $fs->delete_area_files($ctx->contextid);
2574                 }
2575             }
2576             $rs->close();
2577             mtrace('done.');
2579             mtrace('Deleting trash files... ', '');
2580             cron_trace_time_and_memory();
2581             fulldelete($this->trashdir);
2582             set_config('fileslastcleanup', time());
2583             mtrace('done.');
2584         }
2585     }
2587     /**
2588      * Get the sql formated fields for a file instance to be created from a
2589      * {files} and {files_refernece} join.
2590      *
2591      * @param string $filesprefix the table prefix for the {files} table
2592      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2593      * @return string the sql to go after a SELECT
2594      */
2595     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2596         // Note, these fieldnames MUST NOT overlap between the two tables,
2597         // else problems like MDL-33172 occur.
2598         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2599             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2600             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2602         $referencefields = array('repositoryid' => 'repositoryid',
2603             'reference' => 'reference',
2604             'lastsync' => 'referencelastsync');
2606         // id is specifically named to prevent overlaping between the two tables.
2607         $fields = array();
2608         $fields[] = $filesprefix.'.id AS id';
2609         foreach ($filefields as $field) {
2610             $fields[] = "{$filesprefix}.{$field}";
2611         }
2613         foreach ($referencefields as $field => $alias) {
2614             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2615         }
2617         return implode(', ', $fields);
2618     }
2620     /**
2621      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2622      *
2623      * If the record already exists, its id is returned. If there is no such record yet,
2624      * new one is created (using the lastsync provided, too) and its id is returned.
2625      *
2626      * @param int $repositoryid
2627      * @param string $reference
2628      * @param int $lastsync
2629      * @param int $lifetime argument not used any more
2630      * @return int
2631      */
2632     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2633         global $DB;
2635         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2637         if ($id !== false) {
2638             // bah, that was easy
2639             return $id;
2640         }
2642         // no such record yet, create one
2643         try {
2644             $id = $DB->insert_record('files_reference', array(
2645                 'repositoryid'  => $repositoryid,
2646                 'reference'     => $reference,
2647                 'referencehash' => sha1($reference),
2648                 'lastsync'      => $lastsync));
2649         } catch (dml_exception $e) {
2650             // if inserting the new record failed, chances are that the race condition has just
2651             // occured and the unique index did not allow to create the second record with the same
2652             // repositoryid + reference combo
2653             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2654         }
2656         return $id;
2657     }
2659     /**
2660      * Returns the id of the record in {files_reference} that matches the passed parameters
2661      *
2662      * Depending on the required strictness, false can be returned. The behaviour is consistent
2663      * with standard DML methods.
2664      *
2665      * @param int $repositoryid
2666      * @param string $reference
2667      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2668      * @return int|bool
2669      */
2670     private function get_referencefileid($repositoryid, $reference, $strictness) {
2671         global $DB;
2673         return $DB->get_field('files_reference', 'id',
2674             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2675     }
2677     /**
2678      * Updates a reference to the external resource and all files that use it
2679      *
2680      * This function is called after synchronisation of an external file and updates the
2681      * contenthash, filesize and status of all files that reference this external file
2682      * as well as time last synchronised.
2683      *
2684      * @param int $referencefileid
2685      * @param int $lastsync
2686      * @param int $lifetime argument not used any more, liefetime is returned by repository
2687      * @param string $contenthash
2688      * @param int $filesize
2689      * @param int $status 0 if ok or 666 if source is missing
2690      * @param int $timemodified last time modified of the source, if known
2691      */
2692     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2693         global $DB;
2694         $referencefileid = clean_param($referencefileid, PARAM_INT);
2695         $lastsync = clean_param($lastsync, PARAM_INT);
2696         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2697         $filesize = clean_param($filesize, PARAM_INT);
2698         $status = clean_param($status, PARAM_INT);
2699         $params = array('contenthash' => $contenthash,
2700                     'filesize' => $filesize,
2701                     'status' => $status,
2702                     'referencefileid' => $referencefileid,
2703                     'timemodified' => $timemodified);
2704         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2705             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2706             WHERE referencefileid = :referencefileid', $params);
2707         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2708         $DB->update_record('files_reference', (object)$data);
2709     }