a9640e0e99c34fdc325e40fc5a0686d254a4090e
[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($uniqdir);
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         if (!file_exists($newtmpfile)) {
390             remove_dir($uniqdir);
391             // Cleanup.
392             return false;
393         }
395         $context = context_system::instance();
396         $record = array(
397             'contextid' => $context->id,
398             'component' => 'core',
399             'filearea'  => 'documentconversion',
400             'itemid'    => 0,
401             'filepath'  => '/' . $format . '/',
402             'filename'  => $file->get_contenthash(),
403         );
405         $convertedfile = $this->create_file_from_pathname($record, $newtmpfile);
406         // Cleanup.
407         remove_dir($uniqdir);
408         return $convertedfile;
409     }
411     /**
412      * Returns an image file that represent the given stored file as a preview
413      *
414      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
415      * future, the support for other mimetypes can be added, too (eg. generate an image
416      * preview of PDF, text documents etc).
417      *
418      * @param stored_file $file the file we want to preview
419      * @param string $mode preview mode, eg. 'thumb'
420      * @return stored_file|bool false if unable to create the preview, stored file otherwise
421      */
422     public function get_file_preview(stored_file $file, $mode) {
424         $context = context_system::instance();
425         $path = '/' . trim($mode, '/') . '/';
426         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
428         if (!$preview) {
429             $preview = $this->create_file_preview($file, $mode);
430             if (!$preview) {
431                 return false;
432             }
433         }
435         return $preview;
436     }
438     /**
439      * Return an available file name.
440      *
441      * This will return the next available file name in the area, adding/incrementing a suffix
442      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
443      *
444      * If the file name passed is available without modification, it is returned as is.
445      *
446      * @param int $contextid context ID.
447      * @param string $component component.
448      * @param string $filearea file area.
449      * @param int $itemid area item ID.
450      * @param string $filepath the file path.
451      * @param string $filename the file name.
452      * @return string available file name.
453      * @throws coding_exception if the file name is invalid.
454      * @since Moodle 2.5
455      */
456     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
457         global $DB;
459         // Do not accept '.' or an empty file name (zero is acceptable).
460         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
461             throw new coding_exception('Invalid file name passed', $filename);
462         }
464         // The file does not exist, we return the same file name.
465         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
466             return $filename;
467         }
469         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
470         $pathinfo = pathinfo($filename);
471         $basename = $pathinfo['filename'];
472         $matches = array();
473         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
474             $basename = $matches[1];
475         }
477         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
478         if (isset($pathinfo['extension'])) {
479             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
480         }
482         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
483         $filenamelen = $DB->sql_length('f.filename');
484         $sql = "SELECT filename
485                 FROM {files} f
486                 WHERE
487                     f.contextid = :contextid AND
488                     f.component = :component AND
489                     f.filearea = :filearea AND
490                     f.itemid = :itemid AND
491                     f.filepath = :filepath AND
492                     $filenamelikesql
493                 ORDER BY
494                     $filenamelen DESC,
495                     f.filename DESC";
496         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
497                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
498         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
500         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
501         // would both be returned, but only the one only containing digits should be used.
502         $number = 1;
503         foreach ($results as $result) {
504             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
505             $matches = array();
506             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
507                 $number = $matches[2] + 1;
508                 break;
509             }
510         }
512         // Constructing the new filename.
513         $newfilename = $basename . ' (' . $number . ')';
514         if (isset($pathinfo['extension'])) {
515             $newfilename .= '.' . $pathinfo['extension'];
516         }
518         return $newfilename;
519     }
521     /**
522      * Return an available directory name.
523      *
524      * This will return the next available directory name in the area, adding/incrementing a suffix
525      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
526      *
527      * If the file path passed is available without modification, it is returned as is.
528      *
529      * @param int $contextid context ID.
530      * @param string $component component.
531      * @param string $filearea file area.
532      * @param int $itemid area item ID.
533      * @param string $suggestedpath the suggested file path.
534      * @return string available file path
535      * @since Moodle 2.5
536      */
537     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
538         global $DB;
540         // Ensure suggestedpath has trailing '/'
541         $suggestedpath = rtrim($suggestedpath, '/'). '/';
543         // The directory does not exist, we return the same file path.
544         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
545             return $suggestedpath;
546         }
548         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
549         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
550             $suggestedpath = $matches[1]. '/';
551         }
553         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
555         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
556         $filepathlen = $DB->sql_length('f.filepath');
557         $sql = "SELECT filepath
558                 FROM {files} f
559                 WHERE
560                     f.contextid = :contextid AND
561                     f.component = :component AND
562                     f.filearea = :filearea AND
563                     f.itemid = :itemid AND
564                     f.filename = :filename AND
565                     $filepathlikesql
566                 ORDER BY
567                     $filepathlen DESC,
568                     f.filepath DESC";
569         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
570                 'filename' => '.', 'filepathlike' => $filepathlike);
571         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
573         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
574         // would both be returned, but only the one only containing digits should be used.
575         $number = 1;
576         foreach ($results as $result) {
577             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
578                 $number = (int)($matches[1]) + 1;
579                 break;
580             }
581         }
583         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
584     }
586     /**
587      * Generates a preview image for the stored file
588      *
589      * @param stored_file $file the file we want to preview
590      * @param string $mode preview mode, eg. 'thumb'
591      * @return stored_file|bool the newly created preview file or false
592      */
593     protected function create_file_preview(stored_file $file, $mode) {
595         $mimetype = $file->get_mimetype();
597         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
598             // make a preview of the image
599             $data = $this->create_imagefile_preview($file, $mode);
601         } else {
602             // unable to create the preview of this mimetype yet
603             return false;
604         }
606         if (empty($data)) {
607             return false;
608         }
610         $context = context_system::instance();
611         $record = array(
612             'contextid' => $context->id,
613             'component' => 'core',
614             'filearea'  => 'preview',
615             'itemid'    => 0,
616             'filepath'  => '/' . trim($mode, '/') . '/',
617             'filename'  => $file->get_contenthash(),
618         );
620         $imageinfo = getimagesizefromstring($data);
621         if ($imageinfo) {
622             $record['mimetype'] = $imageinfo['mime'];
623         }
625         return $this->create_file_from_string($record, $data);
626     }
628     /**
629      * Generates a preview for the stored image file
630      *
631      * @param stored_file $file the image we want to preview
632      * @param string $mode preview mode, eg. 'thumb'
633      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
634      */
635     protected function create_imagefile_preview(stored_file $file, $mode) {
636         global $CFG;
637         require_once($CFG->libdir.'/gdlib.php');
639         if ($mode === 'tinyicon') {
640             $data = $file->generate_image_thumbnail(24, 24);
642         } else if ($mode === 'thumb') {
643             $data = $file->generate_image_thumbnail(90, 90);
645         } else if ($mode === 'bigthumb') {
646             $data = $file->generate_image_thumbnail(250, 250);
648         } else {
649             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
650         }
652         return $data;
653     }
655     /**
656      * Fetch file using local file id.
657      *
658      * Please do not rely on file ids, it is usually easier to use
659      * pathname hashes instead.
660      *
661      * @param int $fileid file ID
662      * @return stored_file|bool stored_file instance if exists, false if not
663      */
664     public function get_file_by_id($fileid) {
665         global $DB;
667         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
668                   FROM {files} f
669              LEFT JOIN {files_reference} r
670                        ON f.referencefileid = r.id
671                  WHERE f.id = ?";
672         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
673             return $this->get_file_instance($filerecord);
674         } else {
675             return false;
676         }
677     }
679     /**
680      * Fetch file using local file full pathname hash
681      *
682      * @param string $pathnamehash path name hash
683      * @return stored_file|bool stored_file instance if exists, false if not
684      */
685     public function get_file_by_hash($pathnamehash) {
686         global $DB;
688         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
689                   FROM {files} f
690              LEFT JOIN {files_reference} r
691                        ON f.referencefileid = r.id
692                  WHERE f.pathnamehash = ?";
693         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
694             return $this->get_file_instance($filerecord);
695         } else {
696             return false;
697         }
698     }
700     /**
701      * Fetch locally stored file.
702      *
703      * @param int $contextid context ID
704      * @param string $component component
705      * @param string $filearea file area
706      * @param int $itemid item ID
707      * @param string $filepath file path
708      * @param string $filename file name
709      * @return stored_file|bool stored_file instance if exists, false if not
710      */
711     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
712         $filepath = clean_param($filepath, PARAM_PATH);
713         $filename = clean_param($filename, PARAM_FILE);
715         if ($filename === '') {
716             $filename = '.';
717         }
719         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
720         return $this->get_file_by_hash($pathnamehash);
721     }
723     /**
724      * Are there any files (or directories)
725      *
726      * @param int $contextid context ID
727      * @param string $component component
728      * @param string $filearea file area
729      * @param bool|int $itemid item id or false if all items
730      * @param bool $ignoredirs whether or not ignore directories
731      * @return bool empty
732      */
733     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
734         global $DB;
736         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
737         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
739         if ($itemid !== false) {
740             $params['itemid'] = $itemid;
741             $where .= " AND itemid = :itemid";
742         }
744         if ($ignoredirs) {
745             $sql = "SELECT 'x'
746                       FROM {files}
747                      WHERE $where AND filename <> '.'";
748         } else {
749             $sql = "SELECT 'x'
750                       FROM {files}
751                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
752         }
754         return !$DB->record_exists_sql($sql, $params);
755     }
757     /**
758      * Returns all files belonging to given repository
759      *
760      * @param int $repositoryid
761      * @param string $sort A fragment of SQL to use for sorting
762      */
763     public function get_external_files($repositoryid, $sort = '') {
764         global $DB;
765         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
766                   FROM {files} f
767              LEFT JOIN {files_reference} r
768                        ON f.referencefileid = r.id
769                  WHERE r.repositoryid = ?";
770         if (!empty($sort)) {
771             $sql .= " ORDER BY {$sort}";
772         }
774         $result = array();
775         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
776         foreach ($filerecords as $filerecord) {
777             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
778         }
779         return $result;
780     }
782     /**
783      * Returns all area files (optionally limited by itemid)
784      *
785      * @param int $contextid context ID
786      * @param string $component component
787      * @param string $filearea file area
788      * @param int $itemid item ID or all files if not specified
789      * @param string $sort A fragment of SQL to use for sorting
790      * @param bool $includedirs whether or not include directories
791      * @return stored_file[] array of stored_files indexed by pathanmehash
792      */
793     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
794         global $DB;
796         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
797         if ($itemid !== false) {
798             $itemidsql = ' AND f.itemid = :itemid ';
799             $conditions['itemid'] = $itemid;
800         } else {
801             $itemidsql = '';
802         }
804         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
805                   FROM {files} f
806              LEFT JOIN {files_reference} r
807                        ON f.referencefileid = r.id
808                  WHERE f.contextid = :contextid
809                        AND f.component = :component
810                        AND f.filearea = :filearea
811                        $itemidsql";
812         if (!empty($sort)) {
813             $sql .= " ORDER BY {$sort}";
814         }
816         $result = array();
817         $filerecords = $DB->get_records_sql($sql, $conditions);
818         foreach ($filerecords as $filerecord) {
819             if (!$includedirs and $filerecord->filename === '.') {
820                 continue;
821             }
822             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
823         }
824         return $result;
825     }
827     /**
828      * Returns array based tree structure of area files
829      *
830      * @param int $contextid context ID
831      * @param string $component component
832      * @param string $filearea file area
833      * @param int $itemid item ID
834      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
835      */
836     public function get_area_tree($contextid, $component, $filearea, $itemid) {
837         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
838         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
839         // first create directory structure
840         foreach ($files as $hash=>$dir) {
841             if (!$dir->is_directory()) {
842                 continue;
843             }
844             unset($files[$hash]);
845             if ($dir->get_filepath() === '/') {
846                 $result['dirfile'] = $dir;
847                 continue;
848             }
849             $parts = explode('/', trim($dir->get_filepath(),'/'));
850             $pointer =& $result;
851             foreach ($parts as $part) {
852                 if ($part === '') {
853                     continue;
854                 }
855                 if (!isset($pointer['subdirs'][$part])) {
856                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
857                 }
858                 $pointer =& $pointer['subdirs'][$part];
859             }
860             $pointer['dirfile'] = $dir;
861             unset($pointer);
862         }
863         foreach ($files as $hash=>$file) {
864             $parts = explode('/', trim($file->get_filepath(),'/'));
865             $pointer =& $result;
866             foreach ($parts as $part) {
867                 if ($part === '') {
868                     continue;
869                 }
870                 $pointer =& $pointer['subdirs'][$part];
871             }
872             $pointer['files'][$file->get_filename()] = $file;
873             unset($pointer);
874         }
875         $result = $this->sort_area_tree($result);
876         return $result;
877     }
879     /**
880      * Sorts the result of {@link file_storage::get_area_tree()}.
881      *
882      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
883      * @return array of sorted results
884      */
885     protected function sort_area_tree($tree) {
886         foreach ($tree as $key => &$value) {
887             if ($key == 'subdirs') {
888                 core_collator::ksort($value, core_collator::SORT_NATURAL);
889                 foreach ($value as $subdirname => &$subtree) {
890                     $subtree = $this->sort_area_tree($subtree);
891                 }
892             } else if ($key == 'files') {
893                 core_collator::ksort($value, core_collator::SORT_NATURAL);
894             }
895         }
896         return $tree;
897     }
899     /**
900      * Returns all files and optionally directories
901      *
902      * @param int $contextid context ID
903      * @param string $component component
904      * @param string $filearea file area
905      * @param int $itemid item ID
906      * @param int $filepath directory path
907      * @param bool $recursive include all subdirectories
908      * @param bool $includedirs include files and directories
909      * @param string $sort A fragment of SQL to use for sorting
910      * @return array of stored_files indexed by pathanmehash
911      */
912     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
913         global $DB;
915         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
916             return array();
917         }
919         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
921         if ($recursive) {
923             $dirs = $includedirs ? "" : "AND filename <> '.'";
924             $length = core_text::strlen($filepath);
926             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
927                       FROM {files} f
928                  LEFT JOIN {files_reference} r
929                            ON f.referencefileid = r.id
930                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
931                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
932                            AND f.id <> :dirid
933                            $dirs
934                            $orderby";
935             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
937             $files = array();
938             $dirs  = array();
939             $filerecords = $DB->get_records_sql($sql, $params);
940             foreach ($filerecords as $filerecord) {
941                 if ($filerecord->filename == '.') {
942                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
943                 } else {
944                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
945                 }
946             }
947             $result = array_merge($dirs, $files);
949         } else {
950             $result = array();
951             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
953             $length = core_text::strlen($filepath);
955             if ($includedirs) {
956                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
957                           FROM {files} f
958                      LEFT JOIN {files_reference} r
959                                ON f.referencefileid = r.id
960                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
961                                AND f.itemid = :itemid AND f.filename = '.'
962                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
963                                AND f.id <> :dirid
964                                $orderby";
965                 $reqlevel = substr_count($filepath, '/') + 1;
966                 $filerecords = $DB->get_records_sql($sql, $params);
967                 foreach ($filerecords as $filerecord) {
968                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
969                         continue;
970                     }
971                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
972                 }
973             }
975             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
976                       FROM {files} f
977                  LEFT JOIN {files_reference} r
978                            ON f.referencefileid = r.id
979                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
980                            AND f.filepath = :filepath AND f.filename <> '.'
981                            $orderby";
983             $filerecords = $DB->get_records_sql($sql, $params);
984             foreach ($filerecords as $filerecord) {
985                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
986             }
987         }
989         return $result;
990     }
992     /**
993      * Delete all area files (optionally limited by itemid).
994      *
995      * @param int $contextid context ID
996      * @param string $component component
997      * @param string $filearea file area or all areas in context if not specified
998      * @param int $itemid item ID or all files if not specified
999      * @return bool success
1000      */
1001     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
1002         global $DB;
1004         $conditions = array('contextid'=>$contextid);
1005         if ($component !== false) {
1006             $conditions['component'] = $component;
1007         }
1008         if ($filearea !== false) {
1009             $conditions['filearea'] = $filearea;
1010         }
1011         if ($itemid !== false) {
1012             $conditions['itemid'] = $itemid;
1013         }
1015         $filerecords = $DB->get_records('files', $conditions);
1016         foreach ($filerecords as $filerecord) {
1017             $this->get_file_instance($filerecord)->delete();
1018         }
1020         return true; // BC only
1021     }
1023     /**
1024      * Delete all the files from certain areas where itemid is limited by an
1025      * arbitrary bit of SQL.
1026      *
1027      * @param int $contextid the id of the context the files belong to. Must be given.
1028      * @param string $component the owning component. Must be given.
1029      * @param string $filearea the file area name. Must be given.
1030      * @param string $itemidstest an SQL fragment that the itemid must match. Used
1031      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
1032      *      and may not used named parameters called contextid, component or filearea.
1033      * @param array $params any query params used by $itemidstest.
1034      */
1035     public function delete_area_files_select($contextid, $component,
1036             $filearea, $itemidstest, array $params = null) {
1037         global $DB;
1039         $where = "contextid = :contextid
1040                 AND component = :component
1041                 AND filearea = :filearea
1042                 AND itemid $itemidstest";
1043         $params['contextid'] = $contextid;
1044         $params['component'] = $component;
1045         $params['filearea'] = $filearea;
1047         $filerecords = $DB->get_recordset_select('files', $where, $params);
1048         foreach ($filerecords as $filerecord) {
1049             $this->get_file_instance($filerecord)->delete();
1050         }
1051         $filerecords->close();
1052     }
1054     /**
1055      * Delete all files associated with the given component.
1056      *
1057      * @param string $component the component owning the file
1058      */
1059     public function delete_component_files($component) {
1060         global $DB;
1062         $filerecords = $DB->get_recordset('files', array('component' => $component));
1063         foreach ($filerecords as $filerecord) {
1064             $this->get_file_instance($filerecord)->delete();
1065         }
1066         $filerecords->close();
1067     }
1069     /**
1070      * Move all the files in a file area from one context to another.
1071      *
1072      * @param int $oldcontextid the context the files are being moved from.
1073      * @param int $newcontextid the context the files are being moved to.
1074      * @param string $component the plugin that these files belong to.
1075      * @param string $filearea the name of the file area.
1076      * @param int $itemid file item ID
1077      * @return int the number of files moved, for information.
1078      */
1079     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
1080         // Note, this code is based on some code that Petr wrote in
1081         // forum_move_attachments in mod/forum/lib.php. I moved it here because
1082         // I needed it in the question code too.
1083         $count = 0;
1085         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
1086         foreach ($oldfiles as $oldfile) {
1087             $filerecord = new stdClass();
1088             $filerecord->contextid = $newcontextid;
1089             $this->create_file_from_storedfile($filerecord, $oldfile);
1090             $count += 1;
1091         }
1093         if ($count) {
1094             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
1095         }
1097         return $count;
1098     }
1100     /**
1101      * Recursively creates directory.
1102      *
1103      * @param int $contextid context ID
1104      * @param string $component component
1105      * @param string $filearea file area
1106      * @param int $itemid item ID
1107      * @param string $filepath file path
1108      * @param int $userid the user ID
1109      * @return bool success
1110      */
1111     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
1112         global $DB;
1114         // validate all parameters, we do not want any rubbish stored in database, right?
1115         if (!is_number($contextid) or $contextid < 1) {
1116             throw new file_exception('storedfileproblem', 'Invalid contextid');
1117         }
1119         $component = clean_param($component, PARAM_COMPONENT);
1120         if (empty($component)) {
1121             throw new file_exception('storedfileproblem', 'Invalid component');
1122         }
1124         $filearea = clean_param($filearea, PARAM_AREA);
1125         if (empty($filearea)) {
1126             throw new file_exception('storedfileproblem', 'Invalid filearea');
1127         }
1129         if (!is_number($itemid) or $itemid < 0) {
1130             throw new file_exception('storedfileproblem', 'Invalid itemid');
1131         }
1133         $filepath = clean_param($filepath, PARAM_PATH);
1134         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1135             // path must start and end with '/'
1136             throw new file_exception('storedfileproblem', 'Invalid file path');
1137         }
1139         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1141         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1142             return $dir_info;
1143         }
1145         static $contenthash = null;
1146         if (!$contenthash) {
1147             $this->add_string_to_pool('');
1148             $contenthash = sha1('');
1149         }
1151         $now = time();
1153         $dir_record = new stdClass();
1154         $dir_record->contextid = $contextid;
1155         $dir_record->component = $component;
1156         $dir_record->filearea  = $filearea;
1157         $dir_record->itemid    = $itemid;
1158         $dir_record->filepath  = $filepath;
1159         $dir_record->filename  = '.';
1160         $dir_record->contenthash  = $contenthash;
1161         $dir_record->filesize  = 0;
1163         $dir_record->timecreated  = $now;
1164         $dir_record->timemodified = $now;
1165         $dir_record->mimetype     = null;
1166         $dir_record->userid       = $userid;
1168         $dir_record->pathnamehash = $pathnamehash;
1170         $DB->insert_record('files', $dir_record);
1171         $dir_info = $this->get_file_by_hash($pathnamehash);
1173         if ($filepath !== '/') {
1174             //recurse to parent dirs
1175             $filepath = trim($filepath, '/');
1176             $filepath = explode('/', $filepath);
1177             array_pop($filepath);
1178             $filepath = implode('/', $filepath);
1179             $filepath = ($filepath === '') ? '/' : "/$filepath/";
1180             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1181         }
1183         return $dir_info;
1184     }
1186     /**
1187      * Add new local file based on existing local file.
1188      *
1189      * @param stdClass|array $filerecord object or array describing changes
1190      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1191      * @return stored_file instance of newly created file
1192      */
1193     public function create_file_from_storedfile($filerecord, $fileorid) {
1194         global $DB;
1196         if ($fileorid instanceof stored_file) {
1197             $fid = $fileorid->get_id();
1198         } else {
1199             $fid = $fileorid;
1200         }
1202         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1204         unset($filerecord['id']);
1205         unset($filerecord['filesize']);
1206         unset($filerecord['contenthash']);
1207         unset($filerecord['pathnamehash']);
1209         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1210                   FROM {files} f
1211              LEFT JOIN {files_reference} r
1212                        ON f.referencefileid = r.id
1213                  WHERE f.id = ?";
1215         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1216             throw new file_exception('storedfileproblem', 'File does not exist');
1217         }
1219         unset($newrecord->id);
1221         foreach ($filerecord as $key => $value) {
1222             // validate all parameters, we do not want any rubbish stored in database, right?
1223             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1224                 throw new file_exception('storedfileproblem', 'Invalid contextid');
1225             }
1227             if ($key == 'component') {
1228                 $value = clean_param($value, PARAM_COMPONENT);
1229                 if (empty($value)) {
1230                     throw new file_exception('storedfileproblem', 'Invalid component');
1231                 }
1232             }
1234             if ($key == 'filearea') {
1235                 $value = clean_param($value, PARAM_AREA);
1236                 if (empty($value)) {
1237                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1238                 }
1239             }
1241             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1242                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1243             }
1246             if ($key == 'filepath') {
1247                 $value = clean_param($value, PARAM_PATH);
1248                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1249                     // path must start and end with '/'
1250                     throw new file_exception('storedfileproblem', 'Invalid file path');
1251                 }
1252             }
1254             if ($key == 'filename') {
1255                 $value = clean_param($value, PARAM_FILE);
1256                 if ($value === '') {
1257                     // path must start and end with '/'
1258                     throw new file_exception('storedfileproblem', 'Invalid file name');
1259                 }
1260             }
1262             if ($key === 'timecreated' or $key === 'timemodified') {
1263                 if (!is_number($value)) {
1264                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1265                 }
1266                 if ($value < 0) {
1267                     //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)
1268                     $value = 0;
1269                 }
1270             }
1272             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1273                 $value = clean_param($value, PARAM_INT);
1274             }
1276             $newrecord->$key = $value;
1277         }
1279         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1281         if ($newrecord->filename === '.') {
1282             // special case - only this function supports directories ;-)
1283             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1284             // update the existing directory with the new data
1285             $newrecord->id = $directory->get_id();
1286             $DB->update_record('files', $newrecord);
1287             return $this->get_file_instance($newrecord);
1288         }
1290         // note: referencefileid is copied from the original file so that
1291         // creating a new file from an existing alias creates new alias implicitly.
1292         // here we just check the database consistency.
1293         if (!empty($newrecord->repositoryid)) {
1294             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1295                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1296             }
1297         }
1299         try {
1300             $newrecord->id = $DB->insert_record('files', $newrecord);
1301         } catch (dml_exception $e) {
1302             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1303                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1304         }
1307         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1309         return $this->get_file_instance($newrecord);
1310     }
1312     /**
1313      * Add new local file.
1314      *
1315      * @param stdClass|array $filerecord object or array describing file
1316      * @param string $url the URL to the file
1317      * @param array $options {@link download_file_content()} options
1318      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1319      * @return stored_file
1320      */
1321     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1323         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1324         $filerecord = (object)$filerecord; // We support arrays too.
1326         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1327         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1328         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1329         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1330         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1331         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1332         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1334         if (!isset($filerecord->filename)) {
1335             $parts = explode('/', $url);
1336             $filename = array_pop($parts);
1337             $filerecord->filename = clean_param($filename, PARAM_FILE);
1338         }
1339         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1340         $filerecord->source = clean_param($source, PARAM_URL);
1342         if ($usetempfile) {
1343             check_dir_exists($this->tempdir);
1344             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1345             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1346             if ($content === false) {
1347                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1348             }
1349             try {
1350                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1351                 @unlink($tmpfile);
1352                 return $newfile;
1353             } catch (Exception $e) {
1354                 @unlink($tmpfile);
1355                 throw $e;
1356             }
1358         } else {
1359             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1360             if ($content === false) {
1361                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1362             }
1363             return $this->create_file_from_string($filerecord, $content);
1364         }
1365     }
1367     /**
1368      * Add new local file.
1369      *
1370      * @param stdClass|array $filerecord object or array describing file
1371      * @param string $pathname path to file or content of file
1372      * @return stored_file
1373      */
1374     public function create_file_from_pathname($filerecord, $pathname) {
1375         global $DB;
1377         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1378         $filerecord = (object)$filerecord; // We support arrays too.
1380         // validate all parameters, we do not want any rubbish stored in database, right?
1381         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1382             throw new file_exception('storedfileproblem', 'Invalid contextid');
1383         }
1385         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1386         if (empty($filerecord->component)) {
1387             throw new file_exception('storedfileproblem', 'Invalid component');
1388         }
1390         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1391         if (empty($filerecord->filearea)) {
1392             throw new file_exception('storedfileproblem', 'Invalid filearea');
1393         }
1395         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1396             throw new file_exception('storedfileproblem', 'Invalid itemid');
1397         }
1399         if (!empty($filerecord->sortorder)) {
1400             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1401                 $filerecord->sortorder = 0;
1402             }
1403         } else {
1404             $filerecord->sortorder = 0;
1405         }
1407         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1408         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1409             // path must start and end with '/'
1410             throw new file_exception('storedfileproblem', 'Invalid file path');
1411         }
1413         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1414         if ($filerecord->filename === '') {
1415             // filename must not be empty
1416             throw new file_exception('storedfileproblem', 'Invalid file name');
1417         }
1419         $now = time();
1420         if (isset($filerecord->timecreated)) {
1421             if (!is_number($filerecord->timecreated)) {
1422                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1423             }
1424             if ($filerecord->timecreated < 0) {
1425                 //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)
1426                 $filerecord->timecreated = 0;
1427             }
1428         } else {
1429             $filerecord->timecreated = $now;
1430         }
1432         if (isset($filerecord->timemodified)) {
1433             if (!is_number($filerecord->timemodified)) {
1434                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1435             }
1436             if ($filerecord->timemodified < 0) {
1437                 //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)
1438                 $filerecord->timemodified = 0;
1439             }
1440         } else {
1441             $filerecord->timemodified = $now;
1442         }
1444         $newrecord = new stdClass();
1446         $newrecord->contextid = $filerecord->contextid;
1447         $newrecord->component = $filerecord->component;
1448         $newrecord->filearea  = $filerecord->filearea;
1449         $newrecord->itemid    = $filerecord->itemid;
1450         $newrecord->filepath  = $filerecord->filepath;
1451         $newrecord->filename  = $filerecord->filename;
1453         $newrecord->timecreated  = $filerecord->timecreated;
1454         $newrecord->timemodified = $filerecord->timemodified;
1455         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1456         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1457         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1458         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1459         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1460         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1461         $newrecord->sortorder    = $filerecord->sortorder;
1463         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1465         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1467         try {
1468             $newrecord->id = $DB->insert_record('files', $newrecord);
1469         } catch (dml_exception $e) {
1470             if ($newfile) {
1471                 $this->deleted_file_cleanup($newrecord->contenthash);
1472             }
1473             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1474                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1475         }
1477         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1479         return $this->get_file_instance($newrecord);
1480     }
1482     /**
1483      * Add new local file.
1484      *
1485      * @param stdClass|array $filerecord object or array describing file
1486      * @param string $content content of file
1487      * @return stored_file
1488      */
1489     public function create_file_from_string($filerecord, $content) {
1490         global $DB;
1492         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1493         $filerecord = (object)$filerecord; // We support arrays too.
1495         // validate all parameters, we do not want any rubbish stored in database, right?
1496         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1497             throw new file_exception('storedfileproblem', 'Invalid contextid');
1498         }
1500         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1501         if (empty($filerecord->component)) {
1502             throw new file_exception('storedfileproblem', 'Invalid component');
1503         }
1505         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1506         if (empty($filerecord->filearea)) {
1507             throw new file_exception('storedfileproblem', 'Invalid filearea');
1508         }
1510         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1511             throw new file_exception('storedfileproblem', 'Invalid itemid');
1512         }
1514         if (!empty($filerecord->sortorder)) {
1515             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1516                 $filerecord->sortorder = 0;
1517             }
1518         } else {
1519             $filerecord->sortorder = 0;
1520         }
1522         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1523         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1524             // path must start and end with '/'
1525             throw new file_exception('storedfileproblem', 'Invalid file path');
1526         }
1528         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1529         if ($filerecord->filename === '') {
1530             // path must start and end with '/'
1531             throw new file_exception('storedfileproblem', 'Invalid file name');
1532         }
1534         $now = time();
1535         if (isset($filerecord->timecreated)) {
1536             if (!is_number($filerecord->timecreated)) {
1537                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1538             }
1539             if ($filerecord->timecreated < 0) {
1540                 //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)
1541                 $filerecord->timecreated = 0;
1542             }
1543         } else {
1544             $filerecord->timecreated = $now;
1545         }
1547         if (isset($filerecord->timemodified)) {
1548             if (!is_number($filerecord->timemodified)) {
1549                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1550             }
1551             if ($filerecord->timemodified < 0) {
1552                 //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)
1553                 $filerecord->timemodified = 0;
1554             }
1555         } else {
1556             $filerecord->timemodified = $now;
1557         }
1559         $newrecord = new stdClass();
1561         $newrecord->contextid = $filerecord->contextid;
1562         $newrecord->component = $filerecord->component;
1563         $newrecord->filearea  = $filerecord->filearea;
1564         $newrecord->itemid    = $filerecord->itemid;
1565         $newrecord->filepath  = $filerecord->filepath;
1566         $newrecord->filename  = $filerecord->filename;
1568         $newrecord->timecreated  = $filerecord->timecreated;
1569         $newrecord->timemodified = $filerecord->timemodified;
1570         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1571         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1572         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1573         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1574         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1575         $newrecord->sortorder    = $filerecord->sortorder;
1577         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1578         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1579         // get mimetype by magic bytes
1580         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1582         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1584         try {
1585             $newrecord->id = $DB->insert_record('files', $newrecord);
1586         } catch (dml_exception $e) {
1587             if ($newfile) {
1588                 $this->deleted_file_cleanup($newrecord->contenthash);
1589             }
1590             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1591                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1592         }
1594         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1596         return $this->get_file_instance($newrecord);
1597     }
1599     /**
1600      * Create a new alias/shortcut file from file reference information
1601      *
1602      * @param stdClass|array $filerecord object or array describing the new file
1603      * @param int $repositoryid the id of the repository that provides the original file
1604      * @param string $reference the information required by the repository to locate the original file
1605      * @param array $options options for creating the new file
1606      * @return stored_file
1607      */
1608     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1609         global $DB;
1611         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1612         $filerecord = (object)$filerecord; // We support arrays too.
1614         // validate all parameters, we do not want any rubbish stored in database, right?
1615         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1616             throw new file_exception('storedfileproblem', 'Invalid contextid');
1617         }
1619         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1620         if (empty($filerecord->component)) {
1621             throw new file_exception('storedfileproblem', 'Invalid component');
1622         }
1624         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1625         if (empty($filerecord->filearea)) {
1626             throw new file_exception('storedfileproblem', 'Invalid filearea');
1627         }
1629         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1630             throw new file_exception('storedfileproblem', 'Invalid itemid');
1631         }
1633         if (!empty($filerecord->sortorder)) {
1634             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1635                 $filerecord->sortorder = 0;
1636             }
1637         } else {
1638             $filerecord->sortorder = 0;
1639         }
1641         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1642         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1643         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1644         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1645         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1646         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1647         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1648         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1649             // Path must start and end with '/'.
1650             throw new file_exception('storedfileproblem', 'Invalid file path');
1651         }
1653         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1654         if ($filerecord->filename === '') {
1655             // Path must start and end with '/'.
1656             throw new file_exception('storedfileproblem', 'Invalid file name');
1657         }
1659         $now = time();
1660         if (isset($filerecord->timecreated)) {
1661             if (!is_number($filerecord->timecreated)) {
1662                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1663             }
1664             if ($filerecord->timecreated < 0) {
1665                 // 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)
1666                 $filerecord->timecreated = 0;
1667             }
1668         } else {
1669             $filerecord->timecreated = $now;
1670         }
1672         if (isset($filerecord->timemodified)) {
1673             if (!is_number($filerecord->timemodified)) {
1674                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1675             }
1676             if ($filerecord->timemodified < 0) {
1677                 // 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)
1678                 $filerecord->timemodified = 0;
1679             }
1680         } else {
1681             $filerecord->timemodified = $now;
1682         }
1684         $transaction = $DB->start_delegated_transaction();
1686         try {
1687             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1688         } catch (Exception $e) {
1689             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1690         }
1692         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1693             // there was specified the contenthash for a file already stored in moodle filepool
1694             if (empty($filerecord->filesize)) {
1695                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1696                 $filerecord->filesize = filesize($filepathname);
1697             } else {
1698                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1699             }
1700         } else {
1701             // atempt to get the result of last synchronisation for this reference
1702             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1703                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1704             if ($lastcontent) {
1705                 $filerecord->contenthash = $lastcontent->contenthash;
1706                 $filerecord->filesize = $lastcontent->filesize;
1707             } else {
1708                 // External file doesn't have content in moodle.
1709                 // So we create an empty file for it.
1710                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1711             }
1712         }
1714         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1716         try {
1717             $filerecord->id = $DB->insert_record('files', $filerecord);
1718         } catch (dml_exception $e) {
1719             if (!empty($newfile)) {
1720                 $this->deleted_file_cleanup($filerecord->contenthash);
1721             }
1722             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1723                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1724         }
1726         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1728         $transaction->allow_commit();
1730         // this will retrieve all reference information from DB as well
1731         return $this->get_file_by_id($filerecord->id);
1732     }
1734     /**
1735      * Creates new image file from existing.
1736      *
1737      * @param stdClass|array $filerecord object or array describing new file
1738      * @param int|stored_file $fid file id or stored file object
1739      * @param int $newwidth in pixels
1740      * @param int $newheight in pixels
1741      * @param bool $keepaspectratio whether or not keep aspect ratio
1742      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1743      * @return stored_file
1744      */
1745     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1746         if (!function_exists('imagecreatefromstring')) {
1747             //Most likely the GD php extension isn't installed
1748             //image conversion cannot succeed
1749             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1750         }
1752         if ($fid instanceof stored_file) {
1753             $fid = $fid->get_id();
1754         }
1756         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1758         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1759             throw new file_exception('storedfileproblem', 'File does not exist');
1760         }
1762         if (!$imageinfo = $file->get_imageinfo()) {
1763             throw new file_exception('storedfileproblem', 'File is not an image');
1764         }
1766         if (!isset($filerecord['filename'])) {
1767             $filerecord['filename'] = $file->get_filename();
1768         }
1770         if (!isset($filerecord['mimetype'])) {
1771             $filerecord['mimetype'] = $imageinfo['mimetype'];
1772         }
1774         $width    = $imageinfo['width'];
1775         $height   = $imageinfo['height'];
1777         if ($keepaspectratio) {
1778             if (0 >= $newwidth and 0 >= $newheight) {
1779                 // no sizes specified
1780                 $newwidth  = $width;
1781                 $newheight = $height;
1783             } else if (0 < $newwidth and 0 < $newheight) {
1784                 $xheight = ($newwidth*($height/$width));
1785                 if ($xheight < $newheight) {
1786                     $newheight = (int)$xheight;
1787                 } else {
1788                     $newwidth = (int)($newheight*($width/$height));
1789                 }
1791             } else if (0 < $newwidth) {
1792                 $newheight = (int)($newwidth*($height/$width));
1794             } else { //0 < $newheight
1795                 $newwidth = (int)($newheight*($width/$height));
1796             }
1798         } else {
1799             if (0 >= $newwidth) {
1800                 $newwidth = $width;
1801             }
1802             if (0 >= $newheight) {
1803                 $newheight = $height;
1804             }
1805         }
1807         // The original image.
1808         $img = imagecreatefromstring($file->get_content());
1810         // A new true color image where we will copy our original image.
1811         $newimg = imagecreatetruecolor($newwidth, $newheight);
1813         // Determine if the file supports transparency.
1814         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1816         // Maintain transparency.
1817         if ($hasalpha) {
1818             imagealphablending($newimg, true);
1820             // Get the current transparent index for the original image.
1821             $colour = imagecolortransparent($img);
1822             if ($colour == -1) {
1823                 // Set a transparent colour index if there's none.
1824                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1825                 // Save full alpha channel.
1826                 imagesavealpha($newimg, true);
1827             }
1828             imagecolortransparent($newimg, $colour);
1829             imagefill($newimg, 0, 0, $colour);
1830         }
1832         // Process the image to be output.
1833         if ($height != $newheight or $width != $newwidth) {
1834             // Resample if the dimensions differ from the original.
1835             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1836                 // weird
1837                 throw new file_exception('storedfileproblem', 'Can not resize image');
1838             }
1839             imagedestroy($img);
1840             $img = $newimg;
1842         } else if ($hasalpha) {
1843             // Just copy to the new image with the alpha channel.
1844             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1845                 // Weird.
1846                 throw new file_exception('storedfileproblem', 'Can not copy image');
1847             }
1848             imagedestroy($img);
1849             $img = $newimg;
1851         } else {
1852             // No particular processing needed for the original image.
1853             imagedestroy($newimg);
1854         }
1856         ob_start();
1857         switch ($filerecord['mimetype']) {
1858             case 'image/gif':
1859                 imagegif($img);
1860                 break;
1862             case 'image/jpeg':
1863                 if (is_null($quality)) {
1864                     imagejpeg($img);
1865                 } else {
1866                     imagejpeg($img, NULL, $quality);
1867                 }
1868                 break;
1870             case 'image/png':
1871                 $quality = (int)$quality;
1873                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1874                 // the latter of which can go to 100, we need to make sure that quality here is
1875                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1876                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1877                 imagepng($img, NULL, $quality, NULL);
1878                 break;
1880             default:
1881                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1882         }
1884         $content = ob_get_contents();
1885         ob_end_clean();
1886         imagedestroy($img);
1888         if (!$content) {
1889             throw new file_exception('storedfileproblem', 'Can not convert image');
1890         }
1892         return $this->create_file_from_string($filerecord, $content);
1893     }
1895     /**
1896      * Add file content to sha1 pool.
1897      *
1898      * @param string $pathname path to file
1899      * @param string $contenthash sha1 hash of content if known (performance only)
1900      * @return array (contenthash, filesize, newfile)
1901      */
1902     public function add_file_to_pool($pathname, $contenthash = NULL) {
1903         global $CFG;
1905         if (!is_readable($pathname)) {
1906             throw new file_exception('storedfilecannotread', '', $pathname);
1907         }
1909         $filesize = filesize($pathname);
1910         if ($filesize === false) {
1911             throw new file_exception('storedfilecannotread', '', $pathname);
1912         }
1914         if (is_null($contenthash)) {
1915             $contenthash = sha1_file($pathname);
1916         } else if ($CFG->debugdeveloper) {
1917             $filehash = sha1_file($pathname);
1918             if ($filehash === false) {
1919                 throw new file_exception('storedfilecannotread', '', $pathname);
1920             }
1921             if ($filehash !== $contenthash) {
1922                 // Hopefully this never happens, if yes we need to fix calling code.
1923                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1924                 $contenthash = $filehash;
1925             }
1926         }
1927         if ($contenthash === false) {
1928             throw new file_exception('storedfilecannotread', '', $pathname);
1929         }
1931         if ($filesize > 0 and $contenthash === sha1('')) {
1932             // Did the file change or is sha1_file() borked for this file?
1933             clearstatcache();
1934             $contenthash = sha1_file($pathname);
1935             $filesize = filesize($pathname);
1937             if ($contenthash === false or $filesize === false) {
1938                 throw new file_exception('storedfilecannotread', '', $pathname);
1939             }
1940             if ($filesize > 0 and $contenthash === sha1('')) {
1941                 // This is very weird...
1942                 throw new file_exception('storedfilecannotread', '', $pathname);
1943             }
1944         }
1946         $hashpath = $this->path_from_hash($contenthash);
1947         $hashfile = "$hashpath/$contenthash";
1949         $newfile = true;
1951         if (file_exists($hashfile)) {
1952             if (filesize($hashfile) === $filesize) {
1953                 return array($contenthash, $filesize, false);
1954             }
1955             if (sha1_file($hashfile) === $contenthash) {
1956                 // Jackpot! We have a sha1 collision.
1957                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1958                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1959                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1960                 throw new file_pool_content_exception($contenthash);
1961             }
1962             debugging("Replacing invalid content file $contenthash");
1963             unlink($hashfile);
1964             $newfile = false;
1965         }
1967         if (!is_dir($hashpath)) {
1968             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1969                 // Permission trouble.
1970                 throw new file_exception('storedfilecannotcreatefiledirs');
1971             }
1972         }
1974         // Let's try to prevent some race conditions.
1976         $prev = ignore_user_abort(true);
1977         @unlink($hashfile.'.tmp');
1978         if (!copy($pathname, $hashfile.'.tmp')) {
1979             // Borked permissions or out of disk space.
1980             ignore_user_abort($prev);
1981             throw new file_exception('storedfilecannotcreatefile');
1982         }
1983         if (filesize($hashfile.'.tmp') !== $filesize) {
1984             // This should not happen.
1985             unlink($hashfile.'.tmp');
1986             ignore_user_abort($prev);
1987             throw new file_exception('storedfilecannotcreatefile');
1988         }
1989         rename($hashfile.'.tmp', $hashfile);
1990         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1991         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1992         ignore_user_abort($prev);
1994         return array($contenthash, $filesize, $newfile);
1995     }
1997     /**
1998      * Add string content to sha1 pool.
1999      *
2000      * @param string $content file content - binary string
2001      * @return array (contenthash, filesize, newfile)
2002      */
2003     public function add_string_to_pool($content) {
2004         global $CFG;
2006         $contenthash = sha1($content);
2007         $filesize = strlen($content); // binary length
2009         $hashpath = $this->path_from_hash($contenthash);
2010         $hashfile = "$hashpath/$contenthash";
2012         $newfile = true;
2014         if (file_exists($hashfile)) {
2015             if (filesize($hashfile) === $filesize) {
2016                 return array($contenthash, $filesize, false);
2017             }
2018             if (sha1_file($hashfile) === $contenthash) {
2019                 // Jackpot! We have a sha1 collision.
2020                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
2021                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
2022                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
2023                 throw new file_pool_content_exception($contenthash);
2024             }
2025             debugging("Replacing invalid content file $contenthash");
2026             unlink($hashfile);
2027             $newfile = false;
2028         }
2030         if (!is_dir($hashpath)) {
2031             if (!mkdir($hashpath, $this->dirpermissions, true)) {
2032                 // Permission trouble.
2033                 throw new file_exception('storedfilecannotcreatefiledirs');
2034             }
2035         }
2037         // Hopefully this works around most potential race conditions.
2039         $prev = ignore_user_abort(true);
2041         if (!empty($CFG->preventfilelocking)) {
2042             $newsize = file_put_contents($hashfile.'.tmp', $content);
2043         } else {
2044             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
2045         }
2047         if ($newsize === false) {
2048             // Borked permissions most likely.
2049             ignore_user_abort($prev);
2050             throw new file_exception('storedfilecannotcreatefile');
2051         }
2052         if (filesize($hashfile.'.tmp') !== $filesize) {
2053             // Out of disk space?
2054             unlink($hashfile.'.tmp');
2055             ignore_user_abort($prev);
2056             throw new file_exception('storedfilecannotcreatefile');
2057         }
2058         rename($hashfile.'.tmp', $hashfile);
2059         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
2060         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
2061         ignore_user_abort($prev);
2063         return array($contenthash, $filesize, $newfile);
2064     }
2066     /**
2067      * Serve file content using X-Sendfile header.
2068      * Please make sure that all headers are already sent
2069      * and the all access control checks passed.
2070      *
2071      * @param string $contenthash sah1 hash of the file content to be served
2072      * @return bool success
2073      */
2074     public function xsendfile($contenthash) {
2075         global $CFG;
2076         require_once("$CFG->libdir/xsendfilelib.php");
2078         $hashpath = $this->path_from_hash($contenthash);
2079         return xsendfile("$hashpath/$contenthash");
2080     }
2082     /**
2083      * Content exists
2084      *
2085      * @param string $contenthash
2086      * @return bool
2087      */
2088     public function content_exists($contenthash) {
2089         $dir = $this->path_from_hash($contenthash);
2090         $filepath = $dir . '/' . $contenthash;
2091         return file_exists($filepath);
2092     }
2094     /**
2095      * Return path to file with given hash.
2096      *
2097      * NOTE: must not be public, files in pool must not be modified
2098      *
2099      * @param string $contenthash content hash
2100      * @return string expected file location
2101      */
2102     protected function path_from_hash($contenthash) {
2103         $l1 = $contenthash[0].$contenthash[1];
2104         $l2 = $contenthash[2].$contenthash[3];
2105         return "$this->filedir/$l1/$l2";
2106     }
2108     /**
2109      * Return path to file with given hash.
2110      *
2111      * NOTE: must not be public, files in pool must not be modified
2112      *
2113      * @param string $contenthash content hash
2114      * @return string expected file location
2115      */
2116     protected function trash_path_from_hash($contenthash) {
2117         $l1 = $contenthash[0].$contenthash[1];
2118         $l2 = $contenthash[2].$contenthash[3];
2119         return "$this->trashdir/$l1/$l2";
2120     }
2122     /**
2123      * Tries to recover missing content of file from trash.
2124      *
2125      * @param stored_file $file stored_file instance
2126      * @return bool success
2127      */
2128     public function try_content_recovery($file) {
2129         $contenthash = $file->get_contenthash();
2130         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
2131         if (!is_readable($trashfile)) {
2132             if (!is_readable($this->trashdir.'/'.$contenthash)) {
2133                 return false;
2134             }
2135             // nice, at least alternative trash file in trash root exists
2136             $trashfile = $this->trashdir.'/'.$contenthash;
2137         }
2138         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
2139             //weird, better fail early
2140             return false;
2141         }
2142         $contentdir  = $this->path_from_hash($contenthash);
2143         $contentfile = $contentdir.'/'.$contenthash;
2144         if (file_exists($contentfile)) {
2145             //strange, no need to recover anything
2146             return true;
2147         }
2148         if (!is_dir($contentdir)) {
2149             if (!mkdir($contentdir, $this->dirpermissions, true)) {
2150                 return false;
2151             }
2152         }
2153         return rename($trashfile, $contentfile);
2154     }
2156     /**
2157      * Marks pool file as candidate for deleting.
2158      *
2159      * DO NOT call directly - reserved for core!!
2160      *
2161      * @param string $contenthash
2162      */
2163     public function deleted_file_cleanup($contenthash) {
2164         global $DB;
2166         if ($contenthash === sha1('')) {
2167             // No need to delete empty content file with sha1('') content hash.
2168             return;
2169         }
2171         //Note: this section is critical - in theory file could be reused at the same
2172         //      time, if this happens we can still recover the file from trash
2173         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
2174             // file content is still used
2175             return;
2176         }
2177         //move content file to trash
2178         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
2179         if (!file_exists($contentfile)) {
2180             //weird, but no problem
2181             return;
2182         }
2183         $trashpath = $this->trash_path_from_hash($contenthash);
2184         $trashfile = $trashpath.'/'.$contenthash;
2185         if (file_exists($trashfile)) {
2186             // we already have this content in trash, no need to move it there
2187             unlink($contentfile);
2188             return;
2189         }
2190         if (!is_dir($trashpath)) {
2191             mkdir($trashpath, $this->dirpermissions, true);
2192         }
2193         rename($contentfile, $trashfile);
2194         chmod($trashfile, $this->filepermissions); // fix permissions if needed
2195     }
2197     /**
2198      * When user referring to a moodle file, we build the reference field
2199      *
2200      * @param array $params
2201      * @return string
2202      */
2203     public static function pack_reference($params) {
2204         $params = (array)$params;
2205         $reference = array();
2206         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
2207         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
2208         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
2209         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
2210         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
2211         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
2212         return base64_encode(serialize($reference));
2213     }
2215     /**
2216      * Unpack reference field
2217      *
2218      * @param string $str
2219      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
2220      * @throws file_reference_exception if the $str does not have the expected format
2221      * @return array
2222      */
2223     public static function unpack_reference($str, $cleanparams = false) {
2224         $decoded = base64_decode($str, true);
2225         if ($decoded === false) {
2226             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
2227         }
2228         $params = @unserialize($decoded); // hide E_NOTICE
2229         if ($params === false) {
2230             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
2231         }
2232         if (is_array($params) && $cleanparams) {
2233             $params = array(
2234                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
2235                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
2236                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
2237                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
2238                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
2239                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
2240             );
2241         }
2242         return $params;
2243     }
2245     /**
2246      * Search through the server files.
2247      *
2248      * The query parameter will be used in conjuction with the SQL directive
2249      * LIKE, so include '%' in it if you need to. This search will always ignore
2250      * user files and directories. Note that the search is case insensitive.
2251      *
2252      * This query can quickly become inefficient so use it sparignly.
2253      *
2254      * @param  string  $query The string used with SQL LIKE.
2255      * @param  integer $from  The offset to start the search at.
2256      * @param  integer $limit The maximum number of results.
2257      * @param  boolean $count When true this methods returns the number of results availabe,
2258      *                        disregarding the parameters $from and $limit.
2259      * @return int|array      Integer when count, otherwise array of stored_file objects.
2260      */
2261     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
2262         global $DB;
2263         $params = array(
2264             'contextlevel' => CONTEXT_USER,
2265             'directory' => '.',
2266             'query' => $query
2267         );
2269         if ($count) {
2270             $select = 'COUNT(1)';
2271         } else {
2272             $select = self::instance_sql_fields('f', 'r');
2273         }
2274         $like = $DB->sql_like('f.filename', ':query', false);
2276         $sql = "SELECT $select
2277                   FROM {files} f
2278              LEFT JOIN {files_reference} r
2279                     ON f.referencefileid = r.id
2280                   JOIN {context} c
2281                     ON f.contextid = c.id
2282                  WHERE c.contextlevel <> :contextlevel
2283                    AND f.filename <> :directory
2284                    AND " . $like . "";
2286         if ($count) {
2287             return $DB->count_records_sql($sql, $params);
2288         }
2290         $sql .= " ORDER BY f.filename";
2292         $result = array();
2293         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2294         foreach ($filerecords as $filerecord) {
2295             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2296         }
2297         $filerecords->close();
2299         return $result;
2300     }
2302     /**
2303      * Returns all aliases that refer to some stored_file via the given reference
2304      *
2305      * All repositories that provide access to a stored_file are expected to use
2306      * {@link self::pack_reference()}. This method can't be used if the given reference
2307      * does not use this format or if you are looking for references to an external file
2308      * (for example it can't be used to search for all aliases that refer to a given
2309      * Dropbox or Box.net file).
2310      *
2311      * Aliases in user draft areas are excluded from the returned list.
2312      *
2313      * @param string $reference identification of the referenced file
2314      * @return array of stored_file indexed by its pathnamehash
2315      */
2316     public function search_references($reference) {
2317         global $DB;
2319         if (is_null($reference)) {
2320             throw new coding_exception('NULL is not a valid reference to an external file');
2321         }
2323         // Give {@link self::unpack_reference()} a chance to throw exception if the
2324         // reference is not in a valid format.
2325         self::unpack_reference($reference);
2327         $referencehash = sha1($reference);
2329         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2330                   FROM {files} f
2331                   JOIN {files_reference} r ON f.referencefileid = r.id
2332                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2333                  WHERE r.referencehash = ?
2334                        AND (f.component <> ? OR f.filearea <> ?)";
2336         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2337         $files = array();
2338         foreach ($rs as $filerecord) {
2339             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2340         }
2342         return $files;
2343     }
2345     /**
2346      * Returns the number of aliases that refer to some stored_file via the given reference
2347      *
2348      * All repositories that provide access to a stored_file are expected to use
2349      * {@link self::pack_reference()}. This method can't be used if the given reference
2350      * does not use this format or if you are looking for references to an external file
2351      * (for example it can't be used to count aliases that refer to a given Dropbox or
2352      * Box.net file).
2353      *
2354      * Aliases in user draft areas are not counted.
2355      *
2356      * @param string $reference identification of the referenced file
2357      * @return int
2358      */
2359     public function search_references_count($reference) {
2360         global $DB;
2362         if (is_null($reference)) {
2363             throw new coding_exception('NULL is not a valid reference to an external file');
2364         }
2366         // Give {@link self::unpack_reference()} a chance to throw exception if the
2367         // reference is not in a valid format.
2368         self::unpack_reference($reference);
2370         $referencehash = sha1($reference);
2372         $sql = "SELECT COUNT(f.id)
2373                   FROM {files} f
2374                   JOIN {files_reference} r ON f.referencefileid = r.id
2375                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2376                  WHERE r.referencehash = ?
2377                        AND (f.component <> ? OR f.filearea <> ?)";
2379         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2380     }
2382     /**
2383      * Returns all aliases that link to the given stored_file
2384      *
2385      * Aliases in user draft areas are excluded from the returned list.
2386      *
2387      * @param stored_file $storedfile
2388      * @return array of stored_file
2389      */
2390     public function get_references_by_storedfile(stored_file $storedfile) {
2391         global $DB;
2393         $params = array();
2394         $params['contextid'] = $storedfile->get_contextid();
2395         $params['component'] = $storedfile->get_component();
2396         $params['filearea']  = $storedfile->get_filearea();
2397         $params['itemid']    = $storedfile->get_itemid();
2398         $params['filename']  = $storedfile->get_filename();
2399         $params['filepath']  = $storedfile->get_filepath();
2401         return $this->search_references(self::pack_reference($params));
2402     }
2404     /**
2405      * Returns the number of aliases that link to the given stored_file
2406      *
2407      * Aliases in user draft areas are not counted.
2408      *
2409      * @param stored_file $storedfile
2410      * @return int
2411      */
2412     public function get_references_count_by_storedfile(stored_file $storedfile) {
2413         global $DB;
2415         $params = array();
2416         $params['contextid'] = $storedfile->get_contextid();
2417         $params['component'] = $storedfile->get_component();
2418         $params['filearea']  = $storedfile->get_filearea();
2419         $params['itemid']    = $storedfile->get_itemid();
2420         $params['filename']  = $storedfile->get_filename();
2421         $params['filepath']  = $storedfile->get_filepath();
2423         return $this->search_references_count(self::pack_reference($params));
2424     }
2426     /**
2427      * Updates all files that are referencing this file with the new contenthash
2428      * and filesize
2429      *
2430      * @param stored_file $storedfile
2431      */
2432     public function update_references_to_storedfile(stored_file $storedfile) {
2433         global $CFG, $DB;
2434         $params = array();
2435         $params['contextid'] = $storedfile->get_contextid();
2436         $params['component'] = $storedfile->get_component();
2437         $params['filearea']  = $storedfile->get_filearea();
2438         $params['itemid']    = $storedfile->get_itemid();
2439         $params['filename']  = $storedfile->get_filename();
2440         $params['filepath']  = $storedfile->get_filepath();
2441         $reference = self::pack_reference($params);
2442         $referencehash = sha1($reference);
2444         $sql = "SELECT repositoryid, id FROM {files_reference}
2445                  WHERE referencehash = ?";
2446         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2448         $now = time();
2449         foreach ($rs as $record) {
2450             $this->update_references($record->id, $now, null,
2451                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2452         }
2453         $rs->close();
2454     }
2456     /**
2457      * Convert file alias to local file
2458      *
2459      * @throws moodle_exception if file could not be downloaded
2460      *
2461      * @param stored_file $storedfile a stored_file instances
2462      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2463      * @return stored_file stored_file
2464      */
2465     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2466         global $CFG;
2467         $storedfile->import_external_file_contents($maxbytes);
2468         $storedfile->delete_reference();
2469         return $storedfile;
2470     }
2472     /**
2473      * Return mimetype by given file pathname
2474      *
2475      * If file has a known extension, we return the mimetype based on extension.
2476      * Otherwise (when possible) we try to get the mimetype from file contents.
2477      *
2478      * @param string $pathname full path to the file
2479      * @param string $filename correct file name with extension, if omitted will be taken from $path
2480      * @return string
2481      */
2482     public static function mimetype($pathname, $filename = null) {
2483         if (empty($filename)) {
2484             $filename = $pathname;
2485         }
2486         $type = mimeinfo('type', $filename);
2487         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2488             $finfo = new finfo(FILEINFO_MIME_TYPE);
2489             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2490         }
2491         return $type;
2492     }
2494     /**
2495      * Cron cleanup job.
2496      */
2497     public function cron() {
2498         global $CFG, $DB;
2499         require_once($CFG->libdir.'/cronlib.php');
2501         // find out all stale draft areas (older than 4 days) and purge them
2502         // those are identified by time stamp of the /. root dir
2503         mtrace('Deleting old draft files... ', '');
2504         cron_trace_time_and_memory();
2505         $old = time() - 60*60*24*4;
2506         $sql = "SELECT *
2507                   FROM {files}
2508                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2509                        AND timecreated < :old";
2510         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2511         foreach ($rs as $dir) {
2512             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2513         }
2514         $rs->close();
2515         mtrace('done.');
2517         // remove orphaned preview files (that is files in the core preview filearea without
2518         // the existing original file)
2519         mtrace('Deleting orphaned preview files... ', '');
2520         cron_trace_time_and_memory();
2521         $sql = "SELECT p.*
2522                   FROM {files} p
2523              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2524                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2525                        AND o.id IS NULL";
2526         $syscontext = context_system::instance();
2527         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2528         foreach ($rs as $orphan) {
2529             $file = $this->get_file_instance($orphan);
2530             if (!$file->is_directory()) {
2531                 $file->delete();
2532             }
2533         }
2534         $rs->close();
2535         mtrace('done.');
2537         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2538         // the existing original file).
2539         mtrace('Deleting orphaned document conversion files... ', '');
2540         cron_trace_time_and_memory();
2541         $sql = "SELECT p.*
2542                   FROM {files} p
2543              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2544                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
2545                        AND o.id IS NULL";
2546         $syscontext = context_system::instance();
2547         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2548         foreach ($rs as $orphan) {
2549             $file = $this->get_file_instance($orphan);
2550             if (!$file->is_directory()) {
2551                 $file->delete();
2552             }
2553         }
2554         $rs->close();
2555         mtrace('done.');
2557         // remove trash pool files once a day
2558         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2559         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2560             require_once($CFG->libdir.'/filelib.php');
2561             // Delete files that are associated with a context that no longer exists.
2562             mtrace('Cleaning up files from deleted contexts... ', '');
2563             cron_trace_time_and_memory();
2564             $sql = "SELECT DISTINCT f.contextid
2565                     FROM {files} f
2566                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2567                     WHERE c.id IS NULL";
2568             $rs = $DB->get_recordset_sql($sql);
2569             if ($rs->valid()) {
2570                 $fs = get_file_storage();
2571                 foreach ($rs as $ctx) {
2572                     $fs->delete_area_files($ctx->contextid);
2573                 }
2574             }
2575             $rs->close();
2576             mtrace('done.');
2578             mtrace('Deleting trash files... ', '');
2579             cron_trace_time_and_memory();
2580             fulldelete($this->trashdir);
2581             set_config('fileslastcleanup', time());
2582             mtrace('done.');
2583         }
2584     }
2586     /**
2587      * Get the sql formated fields for a file instance to be created from a
2588      * {files} and {files_refernece} join.
2589      *
2590      * @param string $filesprefix the table prefix for the {files} table
2591      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2592      * @return string the sql to go after a SELECT
2593      */
2594     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2595         // Note, these fieldnames MUST NOT overlap between the two tables,
2596         // else problems like MDL-33172 occur.
2597         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2598             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2599             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2601         $referencefields = array('repositoryid' => 'repositoryid',
2602             'reference' => 'reference',
2603             'lastsync' => 'referencelastsync');
2605         // id is specifically named to prevent overlaping between the two tables.
2606         $fields = array();
2607         $fields[] = $filesprefix.'.id AS id';
2608         foreach ($filefields as $field) {
2609             $fields[] = "{$filesprefix}.{$field}";
2610         }
2612         foreach ($referencefields as $field => $alias) {
2613             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2614         }
2616         return implode(', ', $fields);
2617     }
2619     /**
2620      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2621      *
2622      * If the record already exists, its id is returned. If there is no such record yet,
2623      * new one is created (using the lastsync provided, too) and its id is returned.
2624      *
2625      * @param int $repositoryid
2626      * @param string $reference
2627      * @param int $lastsync
2628      * @param int $lifetime argument not used any more
2629      * @return int
2630      */
2631     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2632         global $DB;
2634         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2636         if ($id !== false) {
2637             // bah, that was easy
2638             return $id;
2639         }
2641         // no such record yet, create one
2642         try {
2643             $id = $DB->insert_record('files_reference', array(
2644                 'repositoryid'  => $repositoryid,
2645                 'reference'     => $reference,
2646                 'referencehash' => sha1($reference),
2647                 'lastsync'      => $lastsync));
2648         } catch (dml_exception $e) {
2649             // if inserting the new record failed, chances are that the race condition has just
2650             // occured and the unique index did not allow to create the second record with the same
2651             // repositoryid + reference combo
2652             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2653         }
2655         return $id;
2656     }
2658     /**
2659      * Returns the id of the record in {files_reference} that matches the passed parameters
2660      *
2661      * Depending on the required strictness, false can be returned. The behaviour is consistent
2662      * with standard DML methods.
2663      *
2664      * @param int $repositoryid
2665      * @param string $reference
2666      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2667      * @return int|bool
2668      */
2669     private function get_referencefileid($repositoryid, $reference, $strictness) {
2670         global $DB;
2672         return $DB->get_field('files_reference', 'id',
2673             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2674     }
2676     /**
2677      * Updates a reference to the external resource and all files that use it
2678      *
2679      * This function is called after synchronisation of an external file and updates the
2680      * contenthash, filesize and status of all files that reference this external file
2681      * as well as time last synchronised.
2682      *
2683      * @param int $referencefileid
2684      * @param int $lastsync
2685      * @param int $lifetime argument not used any more, liefetime is returned by repository
2686      * @param string $contenthash
2687      * @param int $filesize
2688      * @param int $status 0 if ok or 666 if source is missing
2689      * @param int $timemodified last time modified of the source, if known
2690      */
2691     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2692         global $DB;
2693         $referencefileid = clean_param($referencefileid, PARAM_INT);
2694         $lastsync = clean_param($lastsync, PARAM_INT);
2695         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2696         $filesize = clean_param($filesize, PARAM_INT);
2697         $status = clean_param($status, PARAM_INT);
2698         $params = array('contenthash' => $contenthash,
2699                     'filesize' => $filesize,
2700                     'status' => $status,
2701                     'referencefileid' => $referencefileid,
2702                     'timemodified' => $timemodified);
2703         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2704             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2705             WHERE referencefileid = :referencefileid', $params);
2706         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2707         $DB->update_record('files_reference', (object)$data);
2708     }