MDL-53923 mod_assign: Movement of functions to file_storage.
[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         $unoconvbin = \escapeshellarg($CFG->pathtounoconv);
242         $command = "$unoconvbin --version";
243         exec($command, $output);
244         preg_match('/([0-9]+\.[0-9]+)/', $output[0], $matches);
245         $currentversion = (float)$matches[0];
246         $supportedversion = 0.7;
247         if ($currentversion < $supportedversion) {
248             return false;
249         }
251         return true;
252     }
254     /**
255      * If the test pdf has been generated correctly and send it direct to the browser.
256      */
257     public static function send_test_pdf() {
258         global $CFG;
259         require_once($CFG->libdir . '/filelib.php');
261         $filerecord = array(
262             'contextid' => \context_system::instance()->id,
263             'component' => 'test',
264             'filearea' => 'assignfeedback_editpdf',
265             'itemid' => 0,
266             'filepath' => '/',
267             'filename' => 'unoconv_test.docx'
268         );
270         // Get the fixture doc file content and generate and stored_file object.
271         $fs = get_file_storage();
272         $fixturefile = $CFG->libdir . '/tests/fixtures/unoconv-source.docx';
273         $fixturedata = file_get_contents($fixturefile);
274         $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'],
275                 $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']);
276         if (!$testdocx) {
277             $testdocx = $fs->create_file_from_string($filerecord, $fixturedata);
279         }
281         // Convert the doc file to pdf and send it direct to the browser.
282         $result = $fs->get_converted_document($testdocx, 'pdf');
283         readfile_accel($result, 'application/pdf', true);
284     }
286     /**
287      * Check if unoconv configured path is correct and working.
288      *
289      * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message.
290      */
291     public static function test_unoconv_path() {
292         global $CFG;
293         $unoconvpath = $CFG->pathtounoconv;
295         $ret = new \stdClass();
296         $ret->status = self::UNOCONVPATH_OK;
297         $ret->message = null;
299         if (empty($unoconvpath)) {
300             $ret->status = self::UNOCONVPATH_EMPTY;
301             return $ret;
302         }
303         if (!file_exists($unoconvpath)) {
304             $ret->status = self::UNOCONVPATH_DOESNOTEXIST;
305             return $ret;
306         }
307         if (is_dir($unoconvpath)) {
308             $ret->status = self::UNOCONVPATH_ISDIR;
309             return $ret;
310         }
311         if (!file_is_executable($unoconvpath)) {
312             $ret->status = self::UNOCONVPATH_NOTEXECUTABLE;
313             return $ret;
314         }
315         if (!\file_storage::can_convert_documents()) {
316             $ret->status = self::UNOCONVPATH_VERSIONNOTSUPPORTED;
317             return $ret;
318         }
320         return $ret;
321     }
323     /**
324      * Perform a file format conversion on the specified document.
325      *
326      * @param stored_file $file the file we want to preview
327      * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
328      * @return stored_file|bool false if unable to create the conversion, stored file otherwise
329      */
330     protected function create_converted_document(stored_file $file, $format) {
331         global $CFG;
333         if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
334             // No conversions are possible, sorry.
335             return false;
336         }
338         $fileextension = core_text::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
339         if (!self::is_format_supported_by_unoconv($fileextension)) {
340             return false;
341         }
343         if (!self::is_format_supported_by_unoconv($format)) {
344             return false;
345         }
347         // Copy the file to the local tmp dir.
348         $tmp = make_request_directory();
349         $localfilename = $file->get_filename();
350         // Safety.
351         $localfilename = clean_param($localfilename, PARAM_FILE);
353         $filename = $tmp . '/' . $localfilename;
354         $file->copy_content_to($filename);
356         $newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format;
358         // Safety.
359         $newtmpfile = $tmp . '/' . clean_param($newtmpfile, PARAM_FILE);
361         $cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' .
362                escapeshellarg('-f') . ' ' .
363                escapeshellarg($format) . ' ' .
364                escapeshellarg('-o') . ' ' .
365                escapeshellarg($newtmpfile) . ' ' .
366                escapeshellarg($filename);
368         $e = file_exists($filename);
369         $output = null;
370         $currentdir = getcwd();
371         chdir($tmp);
372         $result = exec($cmd, $output);
373         chdir($currentdir);
374         if (!file_exists($newtmpfile)) {
375             return false;
376         }
378         $context = context_system::instance();
379         $record = array(
380             'contextid' => $context->id,
381             'component' => 'core',
382             'filearea'  => 'documentconversion',
383             'itemid'    => 0,
384             'filepath'  => '/' . $format . '/',
385             'filename'  => $file->get_contenthash(),
386         );
388         return $this->create_file_from_pathname($record, $newtmpfile);
389     }
391     /**
392      * Returns an image file that represent the given stored file as a preview
393      *
394      * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
395      * future, the support for other mimetypes can be added, too (eg. generate an image
396      * preview of PDF, text documents etc).
397      *
398      * @param stored_file $file the file we want to preview
399      * @param string $mode preview mode, eg. 'thumb'
400      * @return stored_file|bool false if unable to create the preview, stored file otherwise
401      */
402     public function get_file_preview(stored_file $file, $mode) {
404         $context = context_system::instance();
405         $path = '/' . trim($mode, '/') . '/';
406         $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
408         if (!$preview) {
409             $preview = $this->create_file_preview($file, $mode);
410             if (!$preview) {
411                 return false;
412             }
413         }
415         return $preview;
416     }
418     /**
419      * Return an available file name.
420      *
421      * This will return the next available file name in the area, adding/incrementing a suffix
422      * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
423      *
424      * If the file name passed is available without modification, it is returned as is.
425      *
426      * @param int $contextid context ID.
427      * @param string $component component.
428      * @param string $filearea file area.
429      * @param int $itemid area item ID.
430      * @param string $filepath the file path.
431      * @param string $filename the file name.
432      * @return string available file name.
433      * @throws coding_exception if the file name is invalid.
434      * @since Moodle 2.5
435      */
436     public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
437         global $DB;
439         // Do not accept '.' or an empty file name (zero is acceptable).
440         if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
441             throw new coding_exception('Invalid file name passed', $filename);
442         }
444         // The file does not exist, we return the same file name.
445         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
446             return $filename;
447         }
449         // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
450         $pathinfo = pathinfo($filename);
451         $basename = $pathinfo['filename'];
452         $matches = array();
453         if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
454             $basename = $matches[1];
455         }
457         $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
458         if (isset($pathinfo['extension'])) {
459             $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
460         }
462         $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
463         $filenamelen = $DB->sql_length('f.filename');
464         $sql = "SELECT filename
465                 FROM {files} f
466                 WHERE
467                     f.contextid = :contextid AND
468                     f.component = :component AND
469                     f.filearea = :filearea AND
470                     f.itemid = :itemid AND
471                     f.filepath = :filepath AND
472                     $filenamelikesql
473                 ORDER BY
474                     $filenamelen DESC,
475                     f.filename DESC";
476         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
477                 'filepath' => $filepath, 'filenamelike' => $filenamelike);
478         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
480         // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
481         // would both be returned, but only the one only containing digits should be used.
482         $number = 1;
483         foreach ($results as $result) {
484             $resultbasename = pathinfo($result, PATHINFO_FILENAME);
485             $matches = array();
486             if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
487                 $number = $matches[2] + 1;
488                 break;
489             }
490         }
492         // Constructing the new filename.
493         $newfilename = $basename . ' (' . $number . ')';
494         if (isset($pathinfo['extension'])) {
495             $newfilename .= '.' . $pathinfo['extension'];
496         }
498         return $newfilename;
499     }
501     /**
502      * Return an available directory name.
503      *
504      * This will return the next available directory name in the area, adding/incrementing a suffix
505      * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc...
506      *
507      * If the file path passed is available without modification, it is returned as is.
508      *
509      * @param int $contextid context ID.
510      * @param string $component component.
511      * @param string $filearea file area.
512      * @param int $itemid area item ID.
513      * @param string $suggestedpath the suggested file path.
514      * @return string available file path
515      * @since Moodle 2.5
516      */
517     public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) {
518         global $DB;
520         // Ensure suggestedpath has trailing '/'
521         $suggestedpath = rtrim($suggestedpath, '/'). '/';
523         // The directory does not exist, we return the same file path.
524         if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) {
525             return $suggestedpath;
526         }
528         // Trying to locate a file path using the used pattern. We remove the used pattern from the path first.
529         if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) {
530             $suggestedpath = $matches[1]. '/';
531         }
533         $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/';
535         $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike');
536         $filepathlen = $DB->sql_length('f.filepath');
537         $sql = "SELECT filepath
538                 FROM {files} f
539                 WHERE
540                     f.contextid = :contextid AND
541                     f.component = :component AND
542                     f.filearea = :filearea AND
543                     f.itemid = :itemid AND
544                     f.filename = :filename AND
545                     $filepathlikesql
546                 ORDER BY
547                     $filepathlen DESC,
548                     f.filepath DESC";
549         $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
550                 'filename' => '.', 'filepathlike' => $filepathlike);
551         $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
553         // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/'
554         // would both be returned, but only the one only containing digits should be used.
555         $number = 1;
556         foreach ($results as $result) {
557             if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) {
558                 $number = (int)($matches[1]) + 1;
559                 break;
560             }
561         }
563         return rtrim($suggestedpath, '/'). ' (' . $number . ')/';
564     }
566     /**
567      * Generates a preview image for the stored file
568      *
569      * @param stored_file $file the file we want to preview
570      * @param string $mode preview mode, eg. 'thumb'
571      * @return stored_file|bool the newly created preview file or false
572      */
573     protected function create_file_preview(stored_file $file, $mode) {
575         $mimetype = $file->get_mimetype();
577         if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
578             // make a preview of the image
579             $data = $this->create_imagefile_preview($file, $mode);
581         } else {
582             // unable to create the preview of this mimetype yet
583             return false;
584         }
586         if (empty($data)) {
587             return false;
588         }
590         $context = context_system::instance();
591         $record = array(
592             'contextid' => $context->id,
593             'component' => 'core',
594             'filearea'  => 'preview',
595             'itemid'    => 0,
596             'filepath'  => '/' . trim($mode, '/') . '/',
597             'filename'  => $file->get_contenthash(),
598         );
600         $imageinfo = getimagesizefromstring($data);
601         if ($imageinfo) {
602             $record['mimetype'] = $imageinfo['mime'];
603         }
605         return $this->create_file_from_string($record, $data);
606     }
608     /**
609      * Generates a preview for the stored image file
610      *
611      * @param stored_file $file the image we want to preview
612      * @param string $mode preview mode, eg. 'thumb'
613      * @return string|bool false if a problem occurs, the thumbnail image data otherwise
614      */
615     protected function create_imagefile_preview(stored_file $file, $mode) {
616         global $CFG;
617         require_once($CFG->libdir.'/gdlib.php');
619         if ($mode === 'tinyicon') {
620             $data = $file->generate_image_thumbnail(24, 24);
622         } else if ($mode === 'thumb') {
623             $data = $file->generate_image_thumbnail(90, 90);
625         } else if ($mode === 'bigthumb') {
626             $data = $file->generate_image_thumbnail(250, 250);
628         } else {
629             throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
630         }
632         return $data;
633     }
635     /**
636      * Fetch file using local file id.
637      *
638      * Please do not rely on file ids, it is usually easier to use
639      * pathname hashes instead.
640      *
641      * @param int $fileid file ID
642      * @return stored_file|bool stored_file instance if exists, false if not
643      */
644     public function get_file_by_id($fileid) {
645         global $DB;
647         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
648                   FROM {files} f
649              LEFT JOIN {files_reference} r
650                        ON f.referencefileid = r.id
651                  WHERE f.id = ?";
652         if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
653             return $this->get_file_instance($filerecord);
654         } else {
655             return false;
656         }
657     }
659     /**
660      * Fetch file using local file full pathname hash
661      *
662      * @param string $pathnamehash path name hash
663      * @return stored_file|bool stored_file instance if exists, false if not
664      */
665     public function get_file_by_hash($pathnamehash) {
666         global $DB;
668         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
669                   FROM {files} f
670              LEFT JOIN {files_reference} r
671                        ON f.referencefileid = r.id
672                  WHERE f.pathnamehash = ?";
673         if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
674             return $this->get_file_instance($filerecord);
675         } else {
676             return false;
677         }
678     }
680     /**
681      * Fetch locally stored file.
682      *
683      * @param int $contextid context ID
684      * @param string $component component
685      * @param string $filearea file area
686      * @param int $itemid item ID
687      * @param string $filepath file path
688      * @param string $filename file name
689      * @return stored_file|bool stored_file instance if exists, false if not
690      */
691     public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
692         $filepath = clean_param($filepath, PARAM_PATH);
693         $filename = clean_param($filename, PARAM_FILE);
695         if ($filename === '') {
696             $filename = '.';
697         }
699         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
700         return $this->get_file_by_hash($pathnamehash);
701     }
703     /**
704      * Are there any files (or directories)
705      *
706      * @param int $contextid context ID
707      * @param string $component component
708      * @param string $filearea file area
709      * @param bool|int $itemid item id or false if all items
710      * @param bool $ignoredirs whether or not ignore directories
711      * @return bool empty
712      */
713     public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
714         global $DB;
716         $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
717         $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
719         if ($itemid !== false) {
720             $params['itemid'] = $itemid;
721             $where .= " AND itemid = :itemid";
722         }
724         if ($ignoredirs) {
725             $sql = "SELECT 'x'
726                       FROM {files}
727                      WHERE $where AND filename <> '.'";
728         } else {
729             $sql = "SELECT 'x'
730                       FROM {files}
731                      WHERE $where AND (filename <> '.' OR filepath <> '/')";
732         }
734         return !$DB->record_exists_sql($sql, $params);
735     }
737     /**
738      * Returns all files belonging to given repository
739      *
740      * @param int $repositoryid
741      * @param string $sort A fragment of SQL to use for sorting
742      */
743     public function get_external_files($repositoryid, $sort = '') {
744         global $DB;
745         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
746                   FROM {files} f
747              LEFT JOIN {files_reference} r
748                        ON f.referencefileid = r.id
749                  WHERE r.repositoryid = ?";
750         if (!empty($sort)) {
751             $sql .= " ORDER BY {$sort}";
752         }
754         $result = array();
755         $filerecords = $DB->get_records_sql($sql, array($repositoryid));
756         foreach ($filerecords as $filerecord) {
757             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
758         }
759         return $result;
760     }
762     /**
763      * Returns all area files (optionally limited by itemid)
764      *
765      * @param int $contextid context ID
766      * @param string $component component
767      * @param string $filearea file area
768      * @param int $itemid item ID or all files if not specified
769      * @param string $sort A fragment of SQL to use for sorting
770      * @param bool $includedirs whether or not include directories
771      * @return stored_file[] array of stored_files indexed by pathanmehash
772      */
773     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
774         global $DB;
776         $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
777         if ($itemid !== false) {
778             $itemidsql = ' AND f.itemid = :itemid ';
779             $conditions['itemid'] = $itemid;
780         } else {
781             $itemidsql = '';
782         }
784         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
785                   FROM {files} f
786              LEFT JOIN {files_reference} r
787                        ON f.referencefileid = r.id
788                  WHERE f.contextid = :contextid
789                        AND f.component = :component
790                        AND f.filearea = :filearea
791                        $itemidsql";
792         if (!empty($sort)) {
793             $sql .= " ORDER BY {$sort}";
794         }
796         $result = array();
797         $filerecords = $DB->get_records_sql($sql, $conditions);
798         foreach ($filerecords as $filerecord) {
799             if (!$includedirs and $filerecord->filename === '.') {
800                 continue;
801             }
802             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
803         }
804         return $result;
805     }
807     /**
808      * Returns array based tree structure of area files
809      *
810      * @param int $contextid context ID
811      * @param string $component component
812      * @param string $filearea file area
813      * @param int $itemid item ID
814      * @return array each dir represented by dirname, subdirs, files and dirfile array elements
815      */
816     public function get_area_tree($contextid, $component, $filearea, $itemid) {
817         $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
818         $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
819         // first create directory structure
820         foreach ($files as $hash=>$dir) {
821             if (!$dir->is_directory()) {
822                 continue;
823             }
824             unset($files[$hash]);
825             if ($dir->get_filepath() === '/') {
826                 $result['dirfile'] = $dir;
827                 continue;
828             }
829             $parts = explode('/', trim($dir->get_filepath(),'/'));
830             $pointer =& $result;
831             foreach ($parts as $part) {
832                 if ($part === '') {
833                     continue;
834                 }
835                 if (!isset($pointer['subdirs'][$part])) {
836                     $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
837                 }
838                 $pointer =& $pointer['subdirs'][$part];
839             }
840             $pointer['dirfile'] = $dir;
841             unset($pointer);
842         }
843         foreach ($files as $hash=>$file) {
844             $parts = explode('/', trim($file->get_filepath(),'/'));
845             $pointer =& $result;
846             foreach ($parts as $part) {
847                 if ($part === '') {
848                     continue;
849                 }
850                 $pointer =& $pointer['subdirs'][$part];
851             }
852             $pointer['files'][$file->get_filename()] = $file;
853             unset($pointer);
854         }
855         $result = $this->sort_area_tree($result);
856         return $result;
857     }
859     /**
860      * Sorts the result of {@link file_storage::get_area_tree()}.
861      *
862      * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
863      * @return array of sorted results
864      */
865     protected function sort_area_tree($tree) {
866         foreach ($tree as $key => &$value) {
867             if ($key == 'subdirs') {
868                 core_collator::ksort($value, core_collator::SORT_NATURAL);
869                 foreach ($value as $subdirname => &$subtree) {
870                     $subtree = $this->sort_area_tree($subtree);
871                 }
872             } else if ($key == 'files') {
873                 core_collator::ksort($value, core_collator::SORT_NATURAL);
874             }
875         }
876         return $tree;
877     }
879     /**
880      * Returns all files and optionally directories
881      *
882      * @param int $contextid context ID
883      * @param string $component component
884      * @param string $filearea file area
885      * @param int $itemid item ID
886      * @param int $filepath directory path
887      * @param bool $recursive include all subdirectories
888      * @param bool $includedirs include files and directories
889      * @param string $sort A fragment of SQL to use for sorting
890      * @return array of stored_files indexed by pathanmehash
891      */
892     public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
893         global $DB;
895         if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
896             return array();
897         }
899         $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
901         if ($recursive) {
903             $dirs = $includedirs ? "" : "AND filename <> '.'";
904             $length = core_text::strlen($filepath);
906             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
907                       FROM {files} f
908                  LEFT JOIN {files_reference} r
909                            ON f.referencefileid = r.id
910                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
911                            AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
912                            AND f.id <> :dirid
913                            $dirs
914                            $orderby";
915             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
917             $files = array();
918             $dirs  = array();
919             $filerecords = $DB->get_records_sql($sql, $params);
920             foreach ($filerecords as $filerecord) {
921                 if ($filerecord->filename == '.') {
922                     $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
923                 } else {
924                     $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
925                 }
926             }
927             $result = array_merge($dirs, $files);
929         } else {
930             $result = array();
931             $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
933             $length = core_text::strlen($filepath);
935             if ($includedirs) {
936                 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
937                           FROM {files} f
938                      LEFT JOIN {files_reference} r
939                                ON f.referencefileid = r.id
940                          WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
941                                AND f.itemid = :itemid AND f.filename = '.'
942                                AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
943                                AND f.id <> :dirid
944                                $orderby";
945                 $reqlevel = substr_count($filepath, '/') + 1;
946                 $filerecords = $DB->get_records_sql($sql, $params);
947                 foreach ($filerecords as $filerecord) {
948                     if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
949                         continue;
950                     }
951                     $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
952                 }
953             }
955             $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
956                       FROM {files} f
957                  LEFT JOIN {files_reference} r
958                            ON f.referencefileid = r.id
959                      WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
960                            AND f.filepath = :filepath AND f.filename <> '.'
961                            $orderby";
963             $filerecords = $DB->get_records_sql($sql, $params);
964             foreach ($filerecords as $filerecord) {
965                 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
966             }
967         }
969         return $result;
970     }
972     /**
973      * Delete all area files (optionally limited by itemid).
974      *
975      * @param int $contextid context ID
976      * @param string $component component
977      * @param string $filearea file area or all areas in context if not specified
978      * @param int $itemid item ID or all files if not specified
979      * @return bool success
980      */
981     public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
982         global $DB;
984         $conditions = array('contextid'=>$contextid);
985         if ($component !== false) {
986             $conditions['component'] = $component;
987         }
988         if ($filearea !== false) {
989             $conditions['filearea'] = $filearea;
990         }
991         if ($itemid !== false) {
992             $conditions['itemid'] = $itemid;
993         }
995         $filerecords = $DB->get_records('files', $conditions);
996         foreach ($filerecords as $filerecord) {
997             $this->get_file_instance($filerecord)->delete();
998         }
1000         return true; // BC only
1001     }
1003     /**
1004      * Delete all the files from certain areas where itemid is limited by an
1005      * arbitrary bit of SQL.
1006      *
1007      * @param int $contextid the id of the context the files belong to. Must be given.
1008      * @param string $component the owning component. Must be given.
1009      * @param string $filearea the file area name. Must be given.
1010      * @param string $itemidstest an SQL fragment that the itemid must match. Used
1011      *      in the query like WHERE itemid $itemidstest. Must used named parameters,
1012      *      and may not used named parameters called contextid, component or filearea.
1013      * @param array $params any query params used by $itemidstest.
1014      */
1015     public function delete_area_files_select($contextid, $component,
1016             $filearea, $itemidstest, array $params = null) {
1017         global $DB;
1019         $where = "contextid = :contextid
1020                 AND component = :component
1021                 AND filearea = :filearea
1022                 AND itemid $itemidstest";
1023         $params['contextid'] = $contextid;
1024         $params['component'] = $component;
1025         $params['filearea'] = $filearea;
1027         $filerecords = $DB->get_recordset_select('files', $where, $params);
1028         foreach ($filerecords as $filerecord) {
1029             $this->get_file_instance($filerecord)->delete();
1030         }
1031         $filerecords->close();
1032     }
1034     /**
1035      * Delete all files associated with the given component.
1036      *
1037      * @param string $component the component owning the file
1038      */
1039     public function delete_component_files($component) {
1040         global $DB;
1042         $filerecords = $DB->get_recordset('files', array('component' => $component));
1043         foreach ($filerecords as $filerecord) {
1044             $this->get_file_instance($filerecord)->delete();
1045         }
1046         $filerecords->close();
1047     }
1049     /**
1050      * Move all the files in a file area from one context to another.
1051      *
1052      * @param int $oldcontextid the context the files are being moved from.
1053      * @param int $newcontextid the context the files are being moved to.
1054      * @param string $component the plugin that these files belong to.
1055      * @param string $filearea the name of the file area.
1056      * @param int $itemid file item ID
1057      * @return int the number of files moved, for information.
1058      */
1059     public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
1060         // Note, this code is based on some code that Petr wrote in
1061         // forum_move_attachments in mod/forum/lib.php. I moved it here because
1062         // I needed it in the question code too.
1063         $count = 0;
1065         $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
1066         foreach ($oldfiles as $oldfile) {
1067             $filerecord = new stdClass();
1068             $filerecord->contextid = $newcontextid;
1069             $this->create_file_from_storedfile($filerecord, $oldfile);
1070             $count += 1;
1071         }
1073         if ($count) {
1074             $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
1075         }
1077         return $count;
1078     }
1080     /**
1081      * Recursively creates directory.
1082      *
1083      * @param int $contextid context ID
1084      * @param string $component component
1085      * @param string $filearea file area
1086      * @param int $itemid item ID
1087      * @param string $filepath file path
1088      * @param int $userid the user ID
1089      * @return bool success
1090      */
1091     public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
1092         global $DB;
1094         // validate all parameters, we do not want any rubbish stored in database, right?
1095         if (!is_number($contextid) or $contextid < 1) {
1096             throw new file_exception('storedfileproblem', 'Invalid contextid');
1097         }
1099         $component = clean_param($component, PARAM_COMPONENT);
1100         if (empty($component)) {
1101             throw new file_exception('storedfileproblem', 'Invalid component');
1102         }
1104         $filearea = clean_param($filearea, PARAM_AREA);
1105         if (empty($filearea)) {
1106             throw new file_exception('storedfileproblem', 'Invalid filearea');
1107         }
1109         if (!is_number($itemid) or $itemid < 0) {
1110             throw new file_exception('storedfileproblem', 'Invalid itemid');
1111         }
1113         $filepath = clean_param($filepath, PARAM_PATH);
1114         if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
1115             // path must start and end with '/'
1116             throw new file_exception('storedfileproblem', 'Invalid file path');
1117         }
1119         $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
1121         if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
1122             return $dir_info;
1123         }
1125         static $contenthash = null;
1126         if (!$contenthash) {
1127             $this->add_string_to_pool('');
1128             $contenthash = sha1('');
1129         }
1131         $now = time();
1133         $dir_record = new stdClass();
1134         $dir_record->contextid = $contextid;
1135         $dir_record->component = $component;
1136         $dir_record->filearea  = $filearea;
1137         $dir_record->itemid    = $itemid;
1138         $dir_record->filepath  = $filepath;
1139         $dir_record->filename  = '.';
1140         $dir_record->contenthash  = $contenthash;
1141         $dir_record->filesize  = 0;
1143         $dir_record->timecreated  = $now;
1144         $dir_record->timemodified = $now;
1145         $dir_record->mimetype     = null;
1146         $dir_record->userid       = $userid;
1148         $dir_record->pathnamehash = $pathnamehash;
1150         $DB->insert_record('files', $dir_record);
1151         $dir_info = $this->get_file_by_hash($pathnamehash);
1153         if ($filepath !== '/') {
1154             //recurse to parent dirs
1155             $filepath = trim($filepath, '/');
1156             $filepath = explode('/', $filepath);
1157             array_pop($filepath);
1158             $filepath = implode('/', $filepath);
1159             $filepath = ($filepath === '') ? '/' : "/$filepath/";
1160             $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
1161         }
1163         return $dir_info;
1164     }
1166     /**
1167      * Add new local file based on existing local file.
1168      *
1169      * @param stdClass|array $filerecord object or array describing changes
1170      * @param stored_file|int $fileorid id or stored_file instance of the existing local file
1171      * @return stored_file instance of newly created file
1172      */
1173     public function create_file_from_storedfile($filerecord, $fileorid) {
1174         global $DB;
1176         if ($fileorid instanceof stored_file) {
1177             $fid = $fileorid->get_id();
1178         } else {
1179             $fid = $fileorid;
1180         }
1182         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1184         unset($filerecord['id']);
1185         unset($filerecord['filesize']);
1186         unset($filerecord['contenthash']);
1187         unset($filerecord['pathnamehash']);
1189         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
1190                   FROM {files} f
1191              LEFT JOIN {files_reference} r
1192                        ON f.referencefileid = r.id
1193                  WHERE f.id = ?";
1195         if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
1196             throw new file_exception('storedfileproblem', 'File does not exist');
1197         }
1199         unset($newrecord->id);
1201         foreach ($filerecord as $key => $value) {
1202             // validate all parameters, we do not want any rubbish stored in database, right?
1203             if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
1204                 throw new file_exception('storedfileproblem', 'Invalid contextid');
1205             }
1207             if ($key == 'component') {
1208                 $value = clean_param($value, PARAM_COMPONENT);
1209                 if (empty($value)) {
1210                     throw new file_exception('storedfileproblem', 'Invalid component');
1211                 }
1212             }
1214             if ($key == 'filearea') {
1215                 $value = clean_param($value, PARAM_AREA);
1216                 if (empty($value)) {
1217                     throw new file_exception('storedfileproblem', 'Invalid filearea');
1218                 }
1219             }
1221             if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
1222                 throw new file_exception('storedfileproblem', 'Invalid itemid');
1223             }
1226             if ($key == 'filepath') {
1227                 $value = clean_param($value, PARAM_PATH);
1228                 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
1229                     // path must start and end with '/'
1230                     throw new file_exception('storedfileproblem', 'Invalid file path');
1231                 }
1232             }
1234             if ($key == 'filename') {
1235                 $value = clean_param($value, PARAM_FILE);
1236                 if ($value === '') {
1237                     // path must start and end with '/'
1238                     throw new file_exception('storedfileproblem', 'Invalid file name');
1239                 }
1240             }
1242             if ($key === 'timecreated' or $key === 'timemodified') {
1243                 if (!is_number($value)) {
1244                     throw new file_exception('storedfileproblem', 'Invalid file '.$key);
1245                 }
1246                 if ($value < 0) {
1247                     //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)
1248                     $value = 0;
1249                 }
1250             }
1252             if ($key == 'referencefileid' or $key == 'referencelastsync') {
1253                 $value = clean_param($value, PARAM_INT);
1254             }
1256             $newrecord->$key = $value;
1257         }
1259         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1261         if ($newrecord->filename === '.') {
1262             // special case - only this function supports directories ;-)
1263             $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1264             // update the existing directory with the new data
1265             $newrecord->id = $directory->get_id();
1266             $DB->update_record('files', $newrecord);
1267             return $this->get_file_instance($newrecord);
1268         }
1270         // note: referencefileid is copied from the original file so that
1271         // creating a new file from an existing alias creates new alias implicitly.
1272         // here we just check the database consistency.
1273         if (!empty($newrecord->repositoryid)) {
1274             if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
1275                 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
1276             }
1277         }
1279         try {
1280             $newrecord->id = $DB->insert_record('files', $newrecord);
1281         } catch (dml_exception $e) {
1282             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1283                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1284         }
1287         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1289         return $this->get_file_instance($newrecord);
1290     }
1292     /**
1293      * Add new local file.
1294      *
1295      * @param stdClass|array $filerecord object or array describing file
1296      * @param string $url the URL to the file
1297      * @param array $options {@link download_file_content()} options
1298      * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
1299      * @return stored_file
1300      */
1301     public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
1303         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1304         $filerecord = (object)$filerecord; // We support arrays too.
1306         $headers        = isset($options['headers'])        ? $options['headers'] : null;
1307         $postdata       = isset($options['postdata'])       ? $options['postdata'] : null;
1308         $fullresponse   = isset($options['fullresponse'])   ? $options['fullresponse'] : false;
1309         $timeout        = isset($options['timeout'])        ? $options['timeout'] : 300;
1310         $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1311         $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
1312         $calctimeout    = isset($options['calctimeout'])    ? $options['calctimeout'] : false;
1314         if (!isset($filerecord->filename)) {
1315             $parts = explode('/', $url);
1316             $filename = array_pop($parts);
1317             $filerecord->filename = clean_param($filename, PARAM_FILE);
1318         }
1319         $source = !empty($filerecord->source) ? $filerecord->source : $url;
1320         $filerecord->source = clean_param($source, PARAM_URL);
1322         if ($usetempfile) {
1323             check_dir_exists($this->tempdir);
1324             $tmpfile = tempnam($this->tempdir, 'newfromurl');
1325             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
1326             if ($content === false) {
1327                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1328             }
1329             try {
1330                 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
1331                 @unlink($tmpfile);
1332                 return $newfile;
1333             } catch (Exception $e) {
1334                 @unlink($tmpfile);
1335                 throw $e;
1336             }
1338         } else {
1339             $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
1340             if ($content === false) {
1341                 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1342             }
1343             return $this->create_file_from_string($filerecord, $content);
1344         }
1345     }
1347     /**
1348      * Add new local file.
1349      *
1350      * @param stdClass|array $filerecord object or array describing file
1351      * @param string $pathname path to file or content of file
1352      * @return stored_file
1353      */
1354     public function create_file_from_pathname($filerecord, $pathname) {
1355         global $DB;
1357         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1358         $filerecord = (object)$filerecord; // We support arrays too.
1360         // validate all parameters, we do not want any rubbish stored in database, right?
1361         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1362             throw new file_exception('storedfileproblem', 'Invalid contextid');
1363         }
1365         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1366         if (empty($filerecord->component)) {
1367             throw new file_exception('storedfileproblem', 'Invalid component');
1368         }
1370         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1371         if (empty($filerecord->filearea)) {
1372             throw new file_exception('storedfileproblem', 'Invalid filearea');
1373         }
1375         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1376             throw new file_exception('storedfileproblem', 'Invalid itemid');
1377         }
1379         if (!empty($filerecord->sortorder)) {
1380             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1381                 $filerecord->sortorder = 0;
1382             }
1383         } else {
1384             $filerecord->sortorder = 0;
1385         }
1387         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1388         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1389             // path must start and end with '/'
1390             throw new file_exception('storedfileproblem', 'Invalid file path');
1391         }
1393         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1394         if ($filerecord->filename === '') {
1395             // filename must not be empty
1396             throw new file_exception('storedfileproblem', 'Invalid file name');
1397         }
1399         $now = time();
1400         if (isset($filerecord->timecreated)) {
1401             if (!is_number($filerecord->timecreated)) {
1402                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1403             }
1404             if ($filerecord->timecreated < 0) {
1405                 //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)
1406                 $filerecord->timecreated = 0;
1407             }
1408         } else {
1409             $filerecord->timecreated = $now;
1410         }
1412         if (isset($filerecord->timemodified)) {
1413             if (!is_number($filerecord->timemodified)) {
1414                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1415             }
1416             if ($filerecord->timemodified < 0) {
1417                 //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)
1418                 $filerecord->timemodified = 0;
1419             }
1420         } else {
1421             $filerecord->timemodified = $now;
1422         }
1424         $newrecord = new stdClass();
1426         $newrecord->contextid = $filerecord->contextid;
1427         $newrecord->component = $filerecord->component;
1428         $newrecord->filearea  = $filerecord->filearea;
1429         $newrecord->itemid    = $filerecord->itemid;
1430         $newrecord->filepath  = $filerecord->filepath;
1431         $newrecord->filename  = $filerecord->filename;
1433         $newrecord->timecreated  = $filerecord->timecreated;
1434         $newrecord->timemodified = $filerecord->timemodified;
1435         $newrecord->mimetype     = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
1436         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1437         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1438         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1439         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1440         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1441         $newrecord->sortorder    = $filerecord->sortorder;
1443         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
1445         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1447         try {
1448             $newrecord->id = $DB->insert_record('files', $newrecord);
1449         } catch (dml_exception $e) {
1450             if ($newfile) {
1451                 $this->deleted_file_cleanup($newrecord->contenthash);
1452             }
1453             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1454                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1455         }
1457         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1459         return $this->get_file_instance($newrecord);
1460     }
1462     /**
1463      * Add new local file.
1464      *
1465      * @param stdClass|array $filerecord object or array describing file
1466      * @param string $content content of file
1467      * @return stored_file
1468      */
1469     public function create_file_from_string($filerecord, $content) {
1470         global $DB;
1472         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1473         $filerecord = (object)$filerecord; // We support arrays too.
1475         // validate all parameters, we do not want any rubbish stored in database, right?
1476         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1477             throw new file_exception('storedfileproblem', 'Invalid contextid');
1478         }
1480         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1481         if (empty($filerecord->component)) {
1482             throw new file_exception('storedfileproblem', 'Invalid component');
1483         }
1485         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1486         if (empty($filerecord->filearea)) {
1487             throw new file_exception('storedfileproblem', 'Invalid filearea');
1488         }
1490         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1491             throw new file_exception('storedfileproblem', 'Invalid itemid');
1492         }
1494         if (!empty($filerecord->sortorder)) {
1495             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1496                 $filerecord->sortorder = 0;
1497             }
1498         } else {
1499             $filerecord->sortorder = 0;
1500         }
1502         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1503         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1504             // path must start and end with '/'
1505             throw new file_exception('storedfileproblem', 'Invalid file path');
1506         }
1508         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1509         if ($filerecord->filename === '') {
1510             // path must start and end with '/'
1511             throw new file_exception('storedfileproblem', 'Invalid file name');
1512         }
1514         $now = time();
1515         if (isset($filerecord->timecreated)) {
1516             if (!is_number($filerecord->timecreated)) {
1517                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1518             }
1519             if ($filerecord->timecreated < 0) {
1520                 //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)
1521                 $filerecord->timecreated = 0;
1522             }
1523         } else {
1524             $filerecord->timecreated = $now;
1525         }
1527         if (isset($filerecord->timemodified)) {
1528             if (!is_number($filerecord->timemodified)) {
1529                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1530             }
1531             if ($filerecord->timemodified < 0) {
1532                 //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)
1533                 $filerecord->timemodified = 0;
1534             }
1535         } else {
1536             $filerecord->timemodified = $now;
1537         }
1539         $newrecord = new stdClass();
1541         $newrecord->contextid = $filerecord->contextid;
1542         $newrecord->component = $filerecord->component;
1543         $newrecord->filearea  = $filerecord->filearea;
1544         $newrecord->itemid    = $filerecord->itemid;
1545         $newrecord->filepath  = $filerecord->filepath;
1546         $newrecord->filename  = $filerecord->filename;
1548         $newrecord->timecreated  = $filerecord->timecreated;
1549         $newrecord->timemodified = $filerecord->timemodified;
1550         $newrecord->userid       = empty($filerecord->userid) ? null : $filerecord->userid;
1551         $newrecord->source       = empty($filerecord->source) ? null : $filerecord->source;
1552         $newrecord->author       = empty($filerecord->author) ? null : $filerecord->author;
1553         $newrecord->license      = empty($filerecord->license) ? null : $filerecord->license;
1554         $newrecord->status       = empty($filerecord->status) ? 0 : $filerecord->status;
1555         $newrecord->sortorder    = $filerecord->sortorder;
1557         list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
1558         $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1559         // get mimetype by magic bytes
1560         $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
1562         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
1564         try {
1565             $newrecord->id = $DB->insert_record('files', $newrecord);
1566         } catch (dml_exception $e) {
1567             if ($newfile) {
1568                 $this->deleted_file_cleanup($newrecord->contenthash);
1569             }
1570             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
1571                                                     $newrecord->filepath, $newrecord->filename, $e->debuginfo);
1572         }
1574         $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
1576         return $this->get_file_instance($newrecord);
1577     }
1579     /**
1580      * Create a new alias/shortcut file from file reference information
1581      *
1582      * @param stdClass|array $filerecord object or array describing the new file
1583      * @param int $repositoryid the id of the repository that provides the original file
1584      * @param string $reference the information required by the repository to locate the original file
1585      * @param array $options options for creating the new file
1586      * @return stored_file
1587      */
1588     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1589         global $DB;
1591         $filerecord = (array)$filerecord;  // Do not modify the submitted record, this cast unlinks objects.
1592         $filerecord = (object)$filerecord; // We support arrays too.
1594         // validate all parameters, we do not want any rubbish stored in database, right?
1595         if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1596             throw new file_exception('storedfileproblem', 'Invalid contextid');
1597         }
1599         $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1600         if (empty($filerecord->component)) {
1601             throw new file_exception('storedfileproblem', 'Invalid component');
1602         }
1604         $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1605         if (empty($filerecord->filearea)) {
1606             throw new file_exception('storedfileproblem', 'Invalid filearea');
1607         }
1609         if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1610             throw new file_exception('storedfileproblem', 'Invalid itemid');
1611         }
1613         if (!empty($filerecord->sortorder)) {
1614             if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1615                 $filerecord->sortorder = 0;
1616             }
1617         } else {
1618             $filerecord->sortorder = 0;
1619         }
1621         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
1622         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
1623         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
1624         $filerecord->author            = empty($filerecord->author) ? null : $filerecord->author;
1625         $filerecord->license           = empty($filerecord->license) ? null : $filerecord->license;
1626         $filerecord->status            = empty($filerecord->status) ? 0 : $filerecord->status;
1627         $filerecord->filepath          = clean_param($filerecord->filepath, PARAM_PATH);
1628         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1629             // Path must start and end with '/'.
1630             throw new file_exception('storedfileproblem', 'Invalid file path');
1631         }
1633         $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1634         if ($filerecord->filename === '') {
1635             // Path must start and end with '/'.
1636             throw new file_exception('storedfileproblem', 'Invalid file name');
1637         }
1639         $now = time();
1640         if (isset($filerecord->timecreated)) {
1641             if (!is_number($filerecord->timecreated)) {
1642                 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1643             }
1644             if ($filerecord->timecreated < 0) {
1645                 // 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)
1646                 $filerecord->timecreated = 0;
1647             }
1648         } else {
1649             $filerecord->timecreated = $now;
1650         }
1652         if (isset($filerecord->timemodified)) {
1653             if (!is_number($filerecord->timemodified)) {
1654                 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1655             }
1656             if ($filerecord->timemodified < 0) {
1657                 // 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)
1658                 $filerecord->timemodified = 0;
1659             }
1660         } else {
1661             $filerecord->timemodified = $now;
1662         }
1664         $transaction = $DB->start_delegated_transaction();
1666         try {
1667             $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
1668         } catch (Exception $e) {
1669             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
1670         }
1672         if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1673             // there was specified the contenthash for a file already stored in moodle filepool
1674             if (empty($filerecord->filesize)) {
1675                 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1676                 $filerecord->filesize = filesize($filepathname);
1677             } else {
1678                 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1679             }
1680         } else {
1681             // atempt to get the result of last synchronisation for this reference
1682             $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1683                     'id, contenthash, filesize', IGNORE_MULTIPLE);
1684             if ($lastcontent) {
1685                 $filerecord->contenthash = $lastcontent->contenthash;
1686                 $filerecord->filesize = $lastcontent->filesize;
1687             } else {
1688                 // External file doesn't have content in moodle.
1689                 // So we create an empty file for it.
1690                 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1691             }
1692         }
1694         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1696         try {
1697             $filerecord->id = $DB->insert_record('files', $filerecord);
1698         } catch (dml_exception $e) {
1699             if (!empty($newfile)) {
1700                 $this->deleted_file_cleanup($filerecord->contenthash);
1701             }
1702             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1703                                                     $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1704         }
1706         $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1708         $transaction->allow_commit();
1710         // this will retrieve all reference information from DB as well
1711         return $this->get_file_by_id($filerecord->id);
1712     }
1714     /**
1715      * Creates new image file from existing.
1716      *
1717      * @param stdClass|array $filerecord object or array describing new file
1718      * @param int|stored_file $fid file id or stored file object
1719      * @param int $newwidth in pixels
1720      * @param int $newheight in pixels
1721      * @param bool $keepaspectratio whether or not keep aspect ratio
1722      * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
1723      * @return stored_file
1724      */
1725     public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
1726         if (!function_exists('imagecreatefromstring')) {
1727             //Most likely the GD php extension isn't installed
1728             //image conversion cannot succeed
1729             throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1730         }
1732         if ($fid instanceof stored_file) {
1733             $fid = $fid->get_id();
1734         }
1736         $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
1738         if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
1739             throw new file_exception('storedfileproblem', 'File does not exist');
1740         }
1742         if (!$imageinfo = $file->get_imageinfo()) {
1743             throw new file_exception('storedfileproblem', 'File is not an image');
1744         }
1746         if (!isset($filerecord['filename'])) {
1747             $filerecord['filename'] = $file->get_filename();
1748         }
1750         if (!isset($filerecord['mimetype'])) {
1751             $filerecord['mimetype'] = $imageinfo['mimetype'];
1752         }
1754         $width    = $imageinfo['width'];
1755         $height   = $imageinfo['height'];
1757         if ($keepaspectratio) {
1758             if (0 >= $newwidth and 0 >= $newheight) {
1759                 // no sizes specified
1760                 $newwidth  = $width;
1761                 $newheight = $height;
1763             } else if (0 < $newwidth and 0 < $newheight) {
1764                 $xheight = ($newwidth*($height/$width));
1765                 if ($xheight < $newheight) {
1766                     $newheight = (int)$xheight;
1767                 } else {
1768                     $newwidth = (int)($newheight*($width/$height));
1769                 }
1771             } else if (0 < $newwidth) {
1772                 $newheight = (int)($newwidth*($height/$width));
1774             } else { //0 < $newheight
1775                 $newwidth = (int)($newheight*($width/$height));
1776             }
1778         } else {
1779             if (0 >= $newwidth) {
1780                 $newwidth = $width;
1781             }
1782             if (0 >= $newheight) {
1783                 $newheight = $height;
1784             }
1785         }
1787         // The original image.
1788         $img = imagecreatefromstring($file->get_content());
1790         // A new true color image where we will copy our original image.
1791         $newimg = imagecreatetruecolor($newwidth, $newheight);
1793         // Determine if the file supports transparency.
1794         $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif';
1796         // Maintain transparency.
1797         if ($hasalpha) {
1798             imagealphablending($newimg, true);
1800             // Get the current transparent index for the original image.
1801             $colour = imagecolortransparent($img);
1802             if ($colour == -1) {
1803                 // Set a transparent colour index if there's none.
1804                 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127);
1805                 // Save full alpha channel.
1806                 imagesavealpha($newimg, true);
1807             }
1808             imagecolortransparent($newimg, $colour);
1809             imagefill($newimg, 0, 0, $colour);
1810         }
1812         // Process the image to be output.
1813         if ($height != $newheight or $width != $newwidth) {
1814             // Resample if the dimensions differ from the original.
1815             if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1816                 // weird
1817                 throw new file_exception('storedfileproblem', 'Can not resize image');
1818             }
1819             imagedestroy($img);
1820             $img = $newimg;
1822         } else if ($hasalpha) {
1823             // Just copy to the new image with the alpha channel.
1824             if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) {
1825                 // Weird.
1826                 throw new file_exception('storedfileproblem', 'Can not copy image');
1827             }
1828             imagedestroy($img);
1829             $img = $newimg;
1831         } else {
1832             // No particular processing needed for the original image.
1833             imagedestroy($newimg);
1834         }
1836         ob_start();
1837         switch ($filerecord['mimetype']) {
1838             case 'image/gif':
1839                 imagegif($img);
1840                 break;
1842             case 'image/jpeg':
1843                 if (is_null($quality)) {
1844                     imagejpeg($img);
1845                 } else {
1846                     imagejpeg($img, NULL, $quality);
1847                 }
1848                 break;
1850             case 'image/png':
1851                 $quality = (int)$quality;
1853                 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality,
1854                 // the latter of which can go to 100, we need to make sure that quality here is
1855                 // in a safe range or PHP WILL CRASH AND DIE. You have been warned.
1856                 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality;
1857                 imagepng($img, NULL, $quality, NULL);
1858                 break;
1860             default:
1861                 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1862         }
1864         $content = ob_get_contents();
1865         ob_end_clean();
1866         imagedestroy($img);
1868         if (!$content) {
1869             throw new file_exception('storedfileproblem', 'Can not convert image');
1870         }
1872         return $this->create_file_from_string($filerecord, $content);
1873     }
1875     /**
1876      * Add file content to sha1 pool.
1877      *
1878      * @param string $pathname path to file
1879      * @param string $contenthash sha1 hash of content if known (performance only)
1880      * @return array (contenthash, filesize, newfile)
1881      */
1882     public function add_file_to_pool($pathname, $contenthash = NULL) {
1883         global $CFG;
1885         if (!is_readable($pathname)) {
1886             throw new file_exception('storedfilecannotread', '', $pathname);
1887         }
1889         $filesize = filesize($pathname);
1890         if ($filesize === false) {
1891             throw new file_exception('storedfilecannotread', '', $pathname);
1892         }
1894         if (is_null($contenthash)) {
1895             $contenthash = sha1_file($pathname);
1896         } else if ($CFG->debugdeveloper) {
1897             $filehash = sha1_file($pathname);
1898             if ($filehash === false) {
1899                 throw new file_exception('storedfilecannotread', '', $pathname);
1900             }
1901             if ($filehash !== $contenthash) {
1902                 // Hopefully this never happens, if yes we need to fix calling code.
1903                 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
1904                 $contenthash = $filehash;
1905             }
1906         }
1907         if ($contenthash === false) {
1908             throw new file_exception('storedfilecannotread', '', $pathname);
1909         }
1911         if ($filesize > 0 and $contenthash === sha1('')) {
1912             // Did the file change or is sha1_file() borked for this file?
1913             clearstatcache();
1914             $contenthash = sha1_file($pathname);
1915             $filesize = filesize($pathname);
1917             if ($contenthash === false or $filesize === false) {
1918                 throw new file_exception('storedfilecannotread', '', $pathname);
1919             }
1920             if ($filesize > 0 and $contenthash === sha1('')) {
1921                 // This is very weird...
1922                 throw new file_exception('storedfilecannotread', '', $pathname);
1923             }
1924         }
1926         $hashpath = $this->path_from_hash($contenthash);
1927         $hashfile = "$hashpath/$contenthash";
1929         $newfile = true;
1931         if (file_exists($hashfile)) {
1932             if (filesize($hashfile) === $filesize) {
1933                 return array($contenthash, $filesize, false);
1934             }
1935             if (sha1_file($hashfile) === $contenthash) {
1936                 // Jackpot! We have a sha1 collision.
1937                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
1938                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
1939                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
1940                 throw new file_pool_content_exception($contenthash);
1941             }
1942             debugging("Replacing invalid content file $contenthash");
1943             unlink($hashfile);
1944             $newfile = false;
1945         }
1947         if (!is_dir($hashpath)) {
1948             if (!mkdir($hashpath, $this->dirpermissions, true)) {
1949                 // Permission trouble.
1950                 throw new file_exception('storedfilecannotcreatefiledirs');
1951             }
1952         }
1954         // Let's try to prevent some race conditions.
1956         $prev = ignore_user_abort(true);
1957         @unlink($hashfile.'.tmp');
1958         if (!copy($pathname, $hashfile.'.tmp')) {
1959             // Borked permissions or out of disk space.
1960             ignore_user_abort($prev);
1961             throw new file_exception('storedfilecannotcreatefile');
1962         }
1963         if (filesize($hashfile.'.tmp') !== $filesize) {
1964             // This should not happen.
1965             unlink($hashfile.'.tmp');
1966             ignore_user_abort($prev);
1967             throw new file_exception('storedfilecannotcreatefile');
1968         }
1969         rename($hashfile.'.tmp', $hashfile);
1970         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
1971         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
1972         ignore_user_abort($prev);
1974         return array($contenthash, $filesize, $newfile);
1975     }
1977     /**
1978      * Add string content to sha1 pool.
1979      *
1980      * @param string $content file content - binary string
1981      * @return array (contenthash, filesize, newfile)
1982      */
1983     public function add_string_to_pool($content) {
1984         global $CFG;
1986         $contenthash = sha1($content);
1987         $filesize = strlen($content); // binary length
1989         $hashpath = $this->path_from_hash($contenthash);
1990         $hashfile = "$hashpath/$contenthash";
1992         $newfile = true;
1994         if (file_exists($hashfile)) {
1995             if (filesize($hashfile) === $filesize) {
1996                 return array($contenthash, $filesize, false);
1997             }
1998             if (sha1_file($hashfile) === $contenthash) {
1999                 // Jackpot! We have a sha1 collision.
2000                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
2001                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
2002                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
2003                 throw new file_pool_content_exception($contenthash);
2004             }
2005             debugging("Replacing invalid content file $contenthash");
2006             unlink($hashfile);
2007             $newfile = false;
2008         }
2010         if (!is_dir($hashpath)) {
2011             if (!mkdir($hashpath, $this->dirpermissions, true)) {
2012                 // Permission trouble.
2013                 throw new file_exception('storedfilecannotcreatefiledirs');
2014             }
2015         }
2017         // Hopefully this works around most potential race conditions.
2019         $prev = ignore_user_abort(true);
2021         if (!empty($CFG->preventfilelocking)) {
2022             $newsize = file_put_contents($hashfile.'.tmp', $content);
2023         } else {
2024             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
2025         }
2027         if ($newsize === false) {
2028             // Borked permissions most likely.
2029             ignore_user_abort($prev);
2030             throw new file_exception('storedfilecannotcreatefile');
2031         }
2032         if (filesize($hashfile.'.tmp') !== $filesize) {
2033             // Out of disk space?
2034             unlink($hashfile.'.tmp');
2035             ignore_user_abort($prev);
2036             throw new file_exception('storedfilecannotcreatefile');
2037         }
2038         rename($hashfile.'.tmp', $hashfile);
2039         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
2040         @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way.
2041         ignore_user_abort($prev);
2043         return array($contenthash, $filesize, $newfile);
2044     }
2046     /**
2047      * Serve file content using X-Sendfile header.
2048      * Please make sure that all headers are already sent
2049      * and the all access control checks passed.
2050      *
2051      * @param string $contenthash sah1 hash of the file content to be served
2052      * @return bool success
2053      */
2054     public function xsendfile($contenthash) {
2055         global $CFG;
2056         require_once("$CFG->libdir/xsendfilelib.php");
2058         $hashpath = $this->path_from_hash($contenthash);
2059         return xsendfile("$hashpath/$contenthash");
2060     }
2062     /**
2063      * Content exists
2064      *
2065      * @param string $contenthash
2066      * @return bool
2067      */
2068     public function content_exists($contenthash) {
2069         $dir = $this->path_from_hash($contenthash);
2070         $filepath = $dir . '/' . $contenthash;
2071         return file_exists($filepath);
2072     }
2074     /**
2075      * Return path to file with given hash.
2076      *
2077      * NOTE: must not be public, files in pool must not be modified
2078      *
2079      * @param string $contenthash content hash
2080      * @return string expected file location
2081      */
2082     protected function path_from_hash($contenthash) {
2083         $l1 = $contenthash[0].$contenthash[1];
2084         $l2 = $contenthash[2].$contenthash[3];
2085         return "$this->filedir/$l1/$l2";
2086     }
2088     /**
2089      * Return path to file with given hash.
2090      *
2091      * NOTE: must not be public, files in pool must not be modified
2092      *
2093      * @param string $contenthash content hash
2094      * @return string expected file location
2095      */
2096     protected function trash_path_from_hash($contenthash) {
2097         $l1 = $contenthash[0].$contenthash[1];
2098         $l2 = $contenthash[2].$contenthash[3];
2099         return "$this->trashdir/$l1/$l2";
2100     }
2102     /**
2103      * Tries to recover missing content of file from trash.
2104      *
2105      * @param stored_file $file stored_file instance
2106      * @return bool success
2107      */
2108     public function try_content_recovery($file) {
2109         $contenthash = $file->get_contenthash();
2110         $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
2111         if (!is_readable($trashfile)) {
2112             if (!is_readable($this->trashdir.'/'.$contenthash)) {
2113                 return false;
2114             }
2115             // nice, at least alternative trash file in trash root exists
2116             $trashfile = $this->trashdir.'/'.$contenthash;
2117         }
2118         if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
2119             //weird, better fail early
2120             return false;
2121         }
2122         $contentdir  = $this->path_from_hash($contenthash);
2123         $contentfile = $contentdir.'/'.$contenthash;
2124         if (file_exists($contentfile)) {
2125             //strange, no need to recover anything
2126             return true;
2127         }
2128         if (!is_dir($contentdir)) {
2129             if (!mkdir($contentdir, $this->dirpermissions, true)) {
2130                 return false;
2131             }
2132         }
2133         return rename($trashfile, $contentfile);
2134     }
2136     /**
2137      * Marks pool file as candidate for deleting.
2138      *
2139      * DO NOT call directly - reserved for core!!
2140      *
2141      * @param string $contenthash
2142      */
2143     public function deleted_file_cleanup($contenthash) {
2144         global $DB;
2146         if ($contenthash === sha1('')) {
2147             // No need to delete empty content file with sha1('') content hash.
2148             return;
2149         }
2151         //Note: this section is critical - in theory file could be reused at the same
2152         //      time, if this happens we can still recover the file from trash
2153         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
2154             // file content is still used
2155             return;
2156         }
2157         //move content file to trash
2158         $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
2159         if (!file_exists($contentfile)) {
2160             //weird, but no problem
2161             return;
2162         }
2163         $trashpath = $this->trash_path_from_hash($contenthash);
2164         $trashfile = $trashpath.'/'.$contenthash;
2165         if (file_exists($trashfile)) {
2166             // we already have this content in trash, no need to move it there
2167             unlink($contentfile);
2168             return;
2169         }
2170         if (!is_dir($trashpath)) {
2171             mkdir($trashpath, $this->dirpermissions, true);
2172         }
2173         rename($contentfile, $trashfile);
2174         chmod($trashfile, $this->filepermissions); // fix permissions if needed
2175     }
2177     /**
2178      * When user referring to a moodle file, we build the reference field
2179      *
2180      * @param array $params
2181      * @return string
2182      */
2183     public static function pack_reference($params) {
2184         $params = (array)$params;
2185         $reference = array();
2186         $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
2187         $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
2188         $reference['itemid']    = is_null($params['itemid'])    ? null : clean_param($params['itemid'],    PARAM_INT);
2189         $reference['filearea']  = is_null($params['filearea'])  ? null : clean_param($params['filearea'],  PARAM_AREA);
2190         $reference['filepath']  = is_null($params['filepath'])  ? null : clean_param($params['filepath'],  PARAM_PATH);
2191         $reference['filename']  = is_null($params['filename'])  ? null : clean_param($params['filename'],  PARAM_FILE);
2192         return base64_encode(serialize($reference));
2193     }
2195     /**
2196      * Unpack reference field
2197      *
2198      * @param string $str
2199      * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
2200      * @throws file_reference_exception if the $str does not have the expected format
2201      * @return array
2202      */
2203     public static function unpack_reference($str, $cleanparams = false) {
2204         $decoded = base64_decode($str, true);
2205         if ($decoded === false) {
2206             throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
2207         }
2208         $params = @unserialize($decoded); // hide E_NOTICE
2209         if ($params === false) {
2210             throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
2211         }
2212         if (is_array($params) && $cleanparams) {
2213             $params = array(
2214                 'component' => is_null($params['component']) ? ''   : clean_param($params['component'], PARAM_COMPONENT),
2215                 'filearea'  => is_null($params['filearea'])  ? ''   : clean_param($params['filearea'], PARAM_AREA),
2216                 'itemid'    => is_null($params['itemid'])    ? 0    : clean_param($params['itemid'], PARAM_INT),
2217                 'filename'  => is_null($params['filename'])  ? null : clean_param($params['filename'], PARAM_FILE),
2218                 'filepath'  => is_null($params['filepath'])  ? null : clean_param($params['filepath'], PARAM_PATH),
2219                 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
2220             );
2221         }
2222         return $params;
2223     }
2225     /**
2226      * Search through the server files.
2227      *
2228      * The query parameter will be used in conjuction with the SQL directive
2229      * LIKE, so include '%' in it if you need to. This search will always ignore
2230      * user files and directories. Note that the search is case insensitive.
2231      *
2232      * This query can quickly become inefficient so use it sparignly.
2233      *
2234      * @param  string  $query The string used with SQL LIKE.
2235      * @param  integer $from  The offset to start the search at.
2236      * @param  integer $limit The maximum number of results.
2237      * @param  boolean $count When true this methods returns the number of results availabe,
2238      *                        disregarding the parameters $from and $limit.
2239      * @return int|array      Integer when count, otherwise array of stored_file objects.
2240      */
2241     public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
2242         global $DB;
2243         $params = array(
2244             'contextlevel' => CONTEXT_USER,
2245             'directory' => '.',
2246             'query' => $query
2247         );
2249         if ($count) {
2250             $select = 'COUNT(1)';
2251         } else {
2252             $select = self::instance_sql_fields('f', 'r');
2253         }
2254         $like = $DB->sql_like('f.filename', ':query', false);
2256         $sql = "SELECT $select
2257                   FROM {files} f
2258              LEFT JOIN {files_reference} r
2259                     ON f.referencefileid = r.id
2260                   JOIN {context} c
2261                     ON f.contextid = c.id
2262                  WHERE c.contextlevel <> :contextlevel
2263                    AND f.filename <> :directory
2264                    AND " . $like . "";
2266         if ($count) {
2267             return $DB->count_records_sql($sql, $params);
2268         }
2270         $sql .= " ORDER BY f.filename";
2272         $result = array();
2273         $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
2274         foreach ($filerecords as $filerecord) {
2275             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2276         }
2277         $filerecords->close();
2279         return $result;
2280     }
2282     /**
2283      * Returns all aliases that refer to some stored_file via the given reference
2284      *
2285      * All repositories that provide access to a stored_file are expected to use
2286      * {@link self::pack_reference()}. This method can't be used if the given reference
2287      * does not use this format or if you are looking for references to an external file
2288      * (for example it can't be used to search for all aliases that refer to a given
2289      * Dropbox or Box.net file).
2290      *
2291      * Aliases in user draft areas are excluded from the returned list.
2292      *
2293      * @param string $reference identification of the referenced file
2294      * @return array of stored_file indexed by its pathnamehash
2295      */
2296     public function search_references($reference) {
2297         global $DB;
2299         if (is_null($reference)) {
2300             throw new coding_exception('NULL is not a valid reference to an external file');
2301         }
2303         // Give {@link self::unpack_reference()} a chance to throw exception if the
2304         // reference is not in a valid format.
2305         self::unpack_reference($reference);
2307         $referencehash = sha1($reference);
2309         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
2310                   FROM {files} f
2311                   JOIN {files_reference} r ON f.referencefileid = r.id
2312                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2313                  WHERE r.referencehash = ?
2314                        AND (f.component <> ? OR f.filearea <> ?)";
2316         $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
2317         $files = array();
2318         foreach ($rs as $filerecord) {
2319             $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
2320         }
2322         return $files;
2323     }
2325     /**
2326      * Returns the number of aliases that refer to some stored_file via the given reference
2327      *
2328      * All repositories that provide access to a stored_file are expected to use
2329      * {@link self::pack_reference()}. This method can't be used if the given reference
2330      * does not use this format or if you are looking for references to an external file
2331      * (for example it can't be used to count aliases that refer to a given Dropbox or
2332      * Box.net file).
2333      *
2334      * Aliases in user draft areas are not counted.
2335      *
2336      * @param string $reference identification of the referenced file
2337      * @return int
2338      */
2339     public function search_references_count($reference) {
2340         global $DB;
2342         if (is_null($reference)) {
2343             throw new coding_exception('NULL is not a valid reference to an external file');
2344         }
2346         // Give {@link self::unpack_reference()} a chance to throw exception if the
2347         // reference is not in a valid format.
2348         self::unpack_reference($reference);
2350         $referencehash = sha1($reference);
2352         $sql = "SELECT COUNT(f.id)
2353                   FROM {files} f
2354                   JOIN {files_reference} r ON f.referencefileid = r.id
2355                   JOIN {repository_instances} ri ON r.repositoryid = ri.id
2356                  WHERE r.referencehash = ?
2357                        AND (f.component <> ? OR f.filearea <> ?)";
2359         return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
2360     }
2362     /**
2363      * Returns all aliases that link to the given stored_file
2364      *
2365      * Aliases in user draft areas are excluded from the returned list.
2366      *
2367      * @param stored_file $storedfile
2368      * @return array of stored_file
2369      */
2370     public function get_references_by_storedfile(stored_file $storedfile) {
2371         global $DB;
2373         $params = array();
2374         $params['contextid'] = $storedfile->get_contextid();
2375         $params['component'] = $storedfile->get_component();
2376         $params['filearea']  = $storedfile->get_filearea();
2377         $params['itemid']    = $storedfile->get_itemid();
2378         $params['filename']  = $storedfile->get_filename();
2379         $params['filepath']  = $storedfile->get_filepath();
2381         return $this->search_references(self::pack_reference($params));
2382     }
2384     /**
2385      * Returns the number of aliases that link to the given stored_file
2386      *
2387      * Aliases in user draft areas are not counted.
2388      *
2389      * @param stored_file $storedfile
2390      * @return int
2391      */
2392     public function get_references_count_by_storedfile(stored_file $storedfile) {
2393         global $DB;
2395         $params = array();
2396         $params['contextid'] = $storedfile->get_contextid();
2397         $params['component'] = $storedfile->get_component();
2398         $params['filearea']  = $storedfile->get_filearea();
2399         $params['itemid']    = $storedfile->get_itemid();
2400         $params['filename']  = $storedfile->get_filename();
2401         $params['filepath']  = $storedfile->get_filepath();
2403         return $this->search_references_count(self::pack_reference($params));
2404     }
2406     /**
2407      * Updates all files that are referencing this file with the new contenthash
2408      * and filesize
2409      *
2410      * @param stored_file $storedfile
2411      */
2412     public function update_references_to_storedfile(stored_file $storedfile) {
2413         global $CFG, $DB;
2414         $params = array();
2415         $params['contextid'] = $storedfile->get_contextid();
2416         $params['component'] = $storedfile->get_component();
2417         $params['filearea']  = $storedfile->get_filearea();
2418         $params['itemid']    = $storedfile->get_itemid();
2419         $params['filename']  = $storedfile->get_filename();
2420         $params['filepath']  = $storedfile->get_filepath();
2421         $reference = self::pack_reference($params);
2422         $referencehash = sha1($reference);
2424         $sql = "SELECT repositoryid, id FROM {files_reference}
2425                  WHERE referencehash = ?";
2426         $rs = $DB->get_recordset_sql($sql, array($referencehash));
2428         $now = time();
2429         foreach ($rs as $record) {
2430             $this->update_references($record->id, $now, null,
2431                     $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified());
2432         }
2433         $rs->close();
2434     }
2436     /**
2437      * Convert file alias to local file
2438      *
2439      * @throws moodle_exception if file could not be downloaded
2440      *
2441      * @param stored_file $storedfile a stored_file instances
2442      * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
2443      * @return stored_file stored_file
2444      */
2445     public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
2446         global $CFG;
2447         $storedfile->import_external_file_contents($maxbytes);
2448         $storedfile->delete_reference();
2449         return $storedfile;
2450     }
2452     /**
2453      * Return mimetype by given file pathname
2454      *
2455      * If file has a known extension, we return the mimetype based on extension.
2456      * Otherwise (when possible) we try to get the mimetype from file contents.
2457      *
2458      * @param string $pathname full path to the file
2459      * @param string $filename correct file name with extension, if omitted will be taken from $path
2460      * @return string
2461      */
2462     public static function mimetype($pathname, $filename = null) {
2463         if (empty($filename)) {
2464             $filename = $pathname;
2465         }
2466         $type = mimeinfo('type', $filename);
2467         if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
2468             $finfo = new finfo(FILEINFO_MIME_TYPE);
2469             $type = mimeinfo_from_type('type', $finfo->file($pathname));
2470         }
2471         return $type;
2472     }
2474     /**
2475      * Cron cleanup job.
2476      */
2477     public function cron() {
2478         global $CFG, $DB;
2479         require_once($CFG->libdir.'/cronlib.php');
2481         // find out all stale draft areas (older than 4 days) and purge them
2482         // those are identified by time stamp of the /. root dir
2483         mtrace('Deleting old draft files... ', '');
2484         cron_trace_time_and_memory();
2485         $old = time() - 60*60*24*4;
2486         $sql = "SELECT *
2487                   FROM {files}
2488                  WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2489                        AND timecreated < :old";
2490         $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2491         foreach ($rs as $dir) {
2492             $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2493         }
2494         $rs->close();
2495         mtrace('done.');
2497         // remove orphaned preview files (that is files in the core preview filearea without
2498         // the existing original file)
2499         mtrace('Deleting orphaned preview files... ', '');
2500         cron_trace_time_and_memory();
2501         $sql = "SELECT p.*
2502                   FROM {files} p
2503              LEFT JOIN {files} o ON (p.filename = o.contenthash)
2504                  WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
2505                        AND o.id IS NULL";
2506         $syscontext = context_system::instance();
2507         $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2508         foreach ($rs as $orphan) {
2509             $file = $this->get_file_instance($orphan);
2510             if (!$file->is_directory()) {
2511                 $file->delete();
2512             }
2513         }
2514         $rs->close();
2515         mtrace('done.');
2517         // Remove orphaned converted files (that is files in the core documentconversion filearea without
2518         // the existing original file).
2519         mtrace('Deleting orphaned document conversion 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 = 'documentconversion' 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 trash pool files once a day
2538         // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2539         if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2540             require_once($CFG->libdir.'/filelib.php');
2541             // Delete files that are associated with a context that no longer exists.
2542             mtrace('Cleaning up files from deleted contexts... ', '');
2543             cron_trace_time_and_memory();
2544             $sql = "SELECT DISTINCT f.contextid
2545                     FROM {files} f
2546                     LEFT OUTER JOIN {context} c ON f.contextid = c.id
2547                     WHERE c.id IS NULL";
2548             $rs = $DB->get_recordset_sql($sql);
2549             if ($rs->valid()) {
2550                 $fs = get_file_storage();
2551                 foreach ($rs as $ctx) {
2552                     $fs->delete_area_files($ctx->contextid);
2553                 }
2554             }
2555             $rs->close();
2556             mtrace('done.');
2558             mtrace('Deleting trash files... ', '');
2559             cron_trace_time_and_memory();
2560             fulldelete($this->trashdir);
2561             set_config('fileslastcleanup', time());
2562             mtrace('done.');
2563         }
2564     }
2566     /**
2567      * Get the sql formated fields for a file instance to be created from a
2568      * {files} and {files_refernece} join.
2569      *
2570      * @param string $filesprefix the table prefix for the {files} table
2571      * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2572      * @return string the sql to go after a SELECT
2573      */
2574     private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2575         // Note, these fieldnames MUST NOT overlap between the two tables,
2576         // else problems like MDL-33172 occur.
2577         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2578             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
2579             'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
2581         $referencefields = array('repositoryid' => 'repositoryid',
2582             'reference' => 'reference',
2583             'lastsync' => 'referencelastsync');
2585         // id is specifically named to prevent overlaping between the two tables.
2586         $fields = array();
2587         $fields[] = $filesprefix.'.id AS id';
2588         foreach ($filefields as $field) {
2589             $fields[] = "{$filesprefix}.{$field}";
2590         }
2592         foreach ($referencefields as $field => $alias) {
2593             $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
2594         }
2596         return implode(', ', $fields);
2597     }
2599     /**
2600      * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2601      *
2602      * If the record already exists, its id is returned. If there is no such record yet,
2603      * new one is created (using the lastsync provided, too) and its id is returned.
2604      *
2605      * @param int $repositoryid
2606      * @param string $reference
2607      * @param int $lastsync
2608      * @param int $lifetime argument not used any more
2609      * @return int
2610      */
2611     private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2612         global $DB;
2614         $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2616         if ($id !== false) {
2617             // bah, that was easy
2618             return $id;
2619         }
2621         // no such record yet, create one
2622         try {
2623             $id = $DB->insert_record('files_reference', array(
2624                 'repositoryid'  => $repositoryid,
2625                 'reference'     => $reference,
2626                 'referencehash' => sha1($reference),
2627                 'lastsync'      => $lastsync));
2628         } catch (dml_exception $e) {
2629             // if inserting the new record failed, chances are that the race condition has just
2630             // occured and the unique index did not allow to create the second record with the same
2631             // repositoryid + reference combo
2632             $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2633         }
2635         return $id;
2636     }
2638     /**
2639      * Returns the id of the record in {files_reference} that matches the passed parameters
2640      *
2641      * Depending on the required strictness, false can be returned. The behaviour is consistent
2642      * with standard DML methods.
2643      *
2644      * @param int $repositoryid
2645      * @param string $reference
2646      * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2647      * @return int|bool
2648      */
2649     private function get_referencefileid($repositoryid, $reference, $strictness) {
2650         global $DB;
2652         return $DB->get_field('files_reference', 'id',
2653             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
2654     }
2656     /**
2657      * Updates a reference to the external resource and all files that use it
2658      *
2659      * This function is called after synchronisation of an external file and updates the
2660      * contenthash, filesize and status of all files that reference this external file
2661      * as well as time last synchronised.
2662      *
2663      * @param int $referencefileid
2664      * @param int $lastsync
2665      * @param int $lifetime argument not used any more, liefetime is returned by repository
2666      * @param string $contenthash
2667      * @param int $filesize
2668      * @param int $status 0 if ok or 666 if source is missing
2669      * @param int $timemodified last time modified of the source, if known
2670      */
2671     public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) {
2672         global $DB;
2673         $referencefileid = clean_param($referencefileid, PARAM_INT);
2674         $lastsync = clean_param($lastsync, PARAM_INT);
2675         validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2676         $filesize = clean_param($filesize, PARAM_INT);
2677         $status = clean_param($status, PARAM_INT);
2678         $params = array('contenthash' => $contenthash,
2679                     'filesize' => $filesize,
2680                     'status' => $status,
2681                     'referencefileid' => $referencefileid,
2682                     'timemodified' => $timemodified);
2683         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2684             status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . '
2685             WHERE referencefileid = :referencefileid', $params);
2686         $data = array('id' => $referencefileid, 'lastsync' => $lastsync);
2687         $DB->update_record('files_reference', (object)$data);
2688     }