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/>.
16
17
18/**
19 * Definition of a class stored_file.
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 */
25
26defined('MOODLE_INTERNAL') || die();
27
28require_once($CFG->dirroot . '/lib/filestorage/file_progress.php');
29require_once($CFG->dirroot . '/lib/filestorage/file_system.php');
30
31/**
32 * Class representing local files stored in a sha1 file pool.
33 *
34 * Since Moodle 2.0 file contents are stored in sha1 pool and
35 * all other file information is stored in new "files" database table.
36 *
37 * @package   core_files
38 * @category  files
39 * @copyright 2008 Petr Skoda {@link http://skodak.org}
40 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 * @since     Moodle 2.0
42 */
43class stored_file {
44    /** @var file_storage file storage pool instance */
45    private $fs;
46    /** @var stdClass record from the files table left join files_reference table */
47    private $file_record;
48    /** @var repository repository plugin instance */
49    private $repository;
50    /** @var file_system filesystem instance */
51    private $filesystem;
52
53    /**
54     * @var int Indicates a file handle of the type returned by fopen.
55     */
56    const FILE_HANDLE_FOPEN = 0;
57
58    /**
59     * @var int Indicates a file handle of the type returned by gzopen.
60     */
61    const FILE_HANDLE_GZOPEN = 1;
62
63
64    /**
65     * Constructor, this constructor should be called ONLY from the file_storage class!
66     *
67     * @param file_storage $fs file  storage instance
68     * @param stdClass $file_record description of file
69     * @param string $deprecated
70     */
71    public function __construct(file_storage $fs, stdClass $file_record, $deprecated = null) {
72        global $DB, $CFG;
73        $this->fs          = $fs;
74        $this->file_record = clone($file_record); // prevent modifications
75
76        if (!empty($file_record->repositoryid)) {
77            require_once("$CFG->dirroot/repository/lib.php");
78            $this->repository = repository::get_repository_by_id($file_record->repositoryid, SYSCONTEXTID);
79            if ($this->repository->supported_returntypes() & FILE_REFERENCE != FILE_REFERENCE) {
80                // Repository cannot do file reference.
81                throw new moodle_exception('error');
82            }
83        } else {
84            $this->repository = null;
85        }
86        // make sure all reference fields exist in file_record even when it is not a reference
87        foreach (array('referencelastsync', 'referencefileid', 'reference', 'repositoryid') as $key) {
88            if (empty($this->file_record->$key)) {
89                $this->file_record->$key = null;
90            }
91        }
92
93        $this->filesystem = $fs->get_file_system();
94    }
95
96    /**
97     * Magic method, called during serialization.
98     *
99     * @return array
100     */
101    public function __sleep() {
102        // We only ever want the file_record saved, not the file_storage object.
103        return ['file_record'];
104    }
105
106    /**
107     * Magic method, called during unserialization.
108     */
109    public function __wakeup() {
110        // Recreate our stored_file based on the file_record, and using file storage retrieved the correct way.
111        $this->__construct(get_file_storage(), $this->file_record);
112    }
113
114    /**
115     * Whether or not this is a external resource
116     *
117     * @return bool
118     */
119    public function is_external_file() {
120        return !empty($this->repository);
121    }
122
123    /**
124     * Whether or not this is a controlled link. Note that repositories cannot support FILE_REFERENCE and FILE_CONTROLLED_LINK.
125     *
126     * @return bool
127     */
128    public function is_controlled_link() {
129        return $this->is_external_file() && $this->repository->supported_returntypes() & FILE_CONTROLLED_LINK;
130    }
131
132    /**
133     * Update some file record fields
134     * NOTE: Must remain protected
135     *
136     * @param stdClass $dataobject
137     */
138    protected function update($dataobject) {
139        global $DB;
140        $updatereferencesneeded = false;
141        $updatemimetype = false;
142        $keys = array_keys((array)$this->file_record);
143        $filepreupdate = clone($this->file_record);
144        foreach ($dataobject as $field => $value) {
145            if (in_array($field, $keys)) {
146                if ($field == 'contextid' and (!is_number($value) or $value < 1)) {
147                    throw new file_exception('storedfileproblem', 'Invalid contextid');
148                }
149
150                if ($field == 'component') {
151                    $value = clean_param($value, PARAM_COMPONENT);
152                    if (empty($value)) {
153                        throw new file_exception('storedfileproblem', 'Invalid component');
154                    }
155                }
156
157                if ($field == 'filearea') {
158                    $value = clean_param($value, PARAM_AREA);
159                    if (empty($value)) {
160                        throw new file_exception('storedfileproblem', 'Invalid filearea');
161                    }
162                }
163
164                if ($field == 'itemid' and (!is_number($value) or $value < 0)) {
165                    throw new file_exception('storedfileproblem', 'Invalid itemid');
166                }
167
168
169                if ($field == 'filepath') {
170                    $value = clean_param($value, PARAM_PATH);
171                    if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
172                        // path must start and end with '/'
173                        throw new file_exception('storedfileproblem', 'Invalid file path');
174                    }
175                }
176
177                if ($field == 'filename') {
178                    // folder has filename == '.', so we pass this
179                    if ($value != '.') {
180                        $value = clean_param($value, PARAM_FILE);
181                    }
182                    if ($value === '') {
183                        throw new file_exception('storedfileproblem', 'Invalid file name');
184                    }
185                }
186
187                if ($field === 'timecreated' or $field === 'timemodified') {
188                    if (!is_number($value)) {
189                        throw new file_exception('storedfileproblem', 'Invalid timestamp');
190                    }
191                    if ($value < 0) {
192                        $value = 0;
193                    }
194                }
195
196                if ($field === 'referencefileid') {
197                    if (!is_null($value) and !is_number($value)) {
198                        throw new file_exception('storedfileproblem', 'Invalid reference info');
199                    }
200                }
201
202                if (($field == 'contenthash' || $field == 'filesize') && $this->file_record->$field != $value) {
203                    $updatereferencesneeded = true;
204                }
205
206                if ($updatereferencesneeded || ($field === 'filename' && $this->file_record->filename != $value)) {
207                    $updatemimetype = true;
208                }
209
210                // adding the field
211                $this->file_record->$field = $value;
212            } else {
213                throw new coding_exception("Invalid field name, $field doesn't exist in file record");
214            }
215        }
216        // Validate mimetype field
217        if ($updatemimetype) {
218            $mimetype = $this->filesystem->mimetype_from_storedfile($this);
219            $this->file_record->mimetype = $mimetype;
220        }
221
222        $DB->update_record('files', $this->file_record);
223        if ($updatereferencesneeded) {
224            // Either filesize or contenthash of this file have changed. Update all files that reference to it.
225            $this->fs->update_references_to_storedfile($this);
226        }
227
228        // Callback for file update.
229        if (!$this->is_directory()) {
230            if ($pluginsfunction = get_plugins_with_function('after_file_updated')) {
231                foreach ($pluginsfunction as $plugintype => $plugins) {
232                    foreach ($plugins as $pluginfunction) {
233                        $pluginfunction($this->file_record, $filepreupdate);
234                    }
235                }
236            }
237        }
238    }
239
240    /**
241     * Rename filename
242     *
243     * @param string $filepath file path
244     * @param string $filename file name
245     */
246    public function rename($filepath, $filename) {
247        if ($this->fs->file_exists($this->get_contextid(), $this->get_component(), $this->get_filearea(), $this->get_itemid(), $filepath, $filename)) {
248            $a = new stdClass();
249            $a->contextid = $this->get_contextid();
250            $a->component = $this->get_component();
251            $a->filearea  = $this->get_filearea();
252            $a->itemid    = $this->get_itemid();
253            $a->filepath  = $filepath;
254            $a->filename  = $filename;
255            throw new file_exception('storedfilenotcreated', $a, 'file exists, cannot rename');
256        }
257        $filerecord = new stdClass;
258        $filerecord->filepath = $filepath;
259        $filerecord->filename = $filename;
260        // populate the pathname hash
261        $filerecord->pathnamehash = $this->fs->get_pathname_hash($this->file_record->contextid, $this->file_record->component, $this->file_record->filearea, $this->file_record->itemid, $filepath, $filename);
262        $this->update($filerecord);
263    }
264
265    /**
266     * Function stored_file::replace_content_with() is deprecated. Please use stored_file::replace_file_with()
267     *
268     * @deprecated since Moodle 2.6 MDL-42016 - please do not use this function any more.
269     * @see stored_file::replace_file_with()
270     */
271    public function replace_content_with(stored_file $storedfile) {
272        throw new coding_exception('Function stored_file::replace_content_with() can not be used any more . ' .
273            'Please use stored_file::replace_file_with()');
274    }
275
276    /**
277     * Replaces the fields that might have changed when file was overriden in filepicker:
278     * reference, contenthash, filesize, userid
279     *
280     * Note that field 'source' must be updated separately because
281     * it has different format for draft and non-draft areas and
282     * this function will usually be used to replace non-draft area
283     * file with draft area file.
284     *
285     * @param stored_file $newfile
286     * @throws coding_exception
287     */
288    public function replace_file_with(stored_file $newfile) {
289        if ($newfile->get_referencefileid() &&
290                $this->fs->get_references_count_by_storedfile($this)) {
291            // The new file is a reference.
292            // The current file has other local files referencing to it.
293            // Double reference is not allowed.
294            throw new moodle_exception('errordoublereference', 'repository');
295        }
296
297        $filerecord = new stdClass;
298        if ($this->filesystem->is_file_readable_remotely_by_storedfile($newfile)) {
299            $contenthash = $newfile->get_contenthash();
300            $filerecord->contenthash = $contenthash;
301        } else {
302            throw new file_exception('storedfileproblem', 'Invalid contenthash, content must be already in filepool', $contenthash);
303        }
304        $filerecord->filesize = $newfile->get_filesize();
305        $filerecord->referencefileid = $newfile->get_referencefileid();
306        $filerecord->userid = $newfile->get_userid();
307        $oldcontenthash = $this->get_contenthash();
308        $this->update($filerecord);
309        $this->filesystem->remove_file($oldcontenthash);
310    }
311
312    /**
313     * Unlink the stored file from the referenced file
314     *
315     * This methods destroys the link to the record in files_reference table. This effectively
316     * turns the stored file from being an alias to a plain copy. However, the caller has
317     * to make sure that the actual file's content has beed synced prior to calling this method.
318     */
319    public function delete_reference() {
320        global $DB;
321
322        if (!$this->is_external_file()) {
323            throw new coding_exception('An attempt to unlink a non-reference file.');
324        }
325
326        $transaction = $DB->start_delegated_transaction();
327
328        // Are we the only one referring to the original file? If so, delete the
329        // referenced file record. Note we do not use file_storage::search_references_count()
330        // here because we want to count draft files too and we are at a bit lower access level here.
331        $countlinks = $DB->count_records('files',
332            array('referencefileid' => $this->file_record->referencefileid));
333        if ($countlinks == 1) {
334            $DB->delete_records('files_reference', array('id' => $this->file_record->referencefileid));
335        }
336
337        // Update the underlying record in the database.
338        $update = new stdClass();
339        $update->referencefileid = null;
340        $this->update($update);
341
342        $transaction->allow_commit();
343
344        // Update our properties and the record in the memory.
345        $this->repository = null;
346        $this->file_record->repositoryid = null;
347        $this->file_record->reference = null;
348        $this->file_record->referencefileid = null;
349        $this->file_record->referencelastsync = null;
350    }
351
352    /**
353     * Is this a directory?
354     *
355     * Directories are only emulated, internally they are stored as empty
356     * files with a "." instead of name - this means empty directory contains
357     * exactly one empty file with name dot.
358     *
359     * @return bool true means directory, false means file
360     */
361    public function is_directory() {
362        return ($this->file_record->filename === '.');
363    }
364
365    /**
366     * Delete file from files table.
367     *
368     * The content of files stored in sha1 pool is reclaimed
369     * later - the occupied disk space is reclaimed much later.
370     *
371     * @return bool always true or exception if error occurred
372     */
373    public function delete() {
374        global $DB;
375
376        if ($this->is_directory()) {
377            // Directories can not be referenced, just delete the record.
378            $DB->delete_records('files', array('id'=>$this->file_record->id));
379
380        } else {
381            $transaction = $DB->start_delegated_transaction();
382
383            // If there are other files referring to this file, convert them to copies.
384            if ($files = $this->fs->get_references_by_storedfile($this)) {
385                foreach ($files as $file) {
386                    $this->fs->import_external_file($file);
387                }
388            }
389
390            // If this file is a reference (alias) to another file, unlink it first.
391            if ($this->is_external_file()) {
392                $this->delete_reference();
393            }
394
395            // Now delete the file record.
396            $DB->delete_records('files', array('id'=>$this->file_record->id));
397
398            $transaction->allow_commit();
399
400            if (!$this->is_directory()) {
401                // Callback for file deletion.
402                if ($pluginsfunction = get_plugins_with_function('after_file_deleted')) {
403                    foreach ($pluginsfunction as $plugintype => $plugins) {
404                        foreach ($plugins as $pluginfunction) {
405                            $pluginfunction($this->file_record);
406                        }
407                    }
408                }
409            }
410        }
411
412        // Move pool file to trash if content not needed any more.
413        $this->filesystem->remove_file($this->file_record->contenthash);
414        return true; // BC only
415    }
416
417    /**
418    * adds this file path to a curl request (POST only)
419    *
420    * @param curl $curlrequest the curl request object
421    * @param string $key what key to use in the POST request
422    * @return void
423    */
424    public function add_to_curl_request(&$curlrequest, $key) {
425        return $this->filesystem->add_to_curl_request($this, $curlrequest, $key);
426    }
427
428    /**
429     * Returns file handle - read only mode, no writing allowed into pool files!
430     *
431     * When you want to modify a file, create a new file and delete the old one.
432     *
433     * @param int $type Type of file handle (FILE_HANDLE_xx constant)
434     * @return resource file handle
435     */
436    public function get_content_file_handle($type = self::FILE_HANDLE_FOPEN) {
437        return $this->filesystem->get_content_file_handle($this, $type);
438    }
439
440    /**
441     * Dumps file content to page.
442     */
443    public function readfile() {
444        return $this->filesystem->readfile($this);
445    }
446
447    /**
448     * Returns file content as string.
449     *
450     * @return string content
451     */
452    public function get_content() {
453        return $this->filesystem->get_content($this);
454    }
455
456    /**
457     * Copy content of file to given pathname.
458     *
459     * @param string $pathname real path to the new file
460     * @return bool success
461     */
462    public function copy_content_to($pathname) {
463        return $this->filesystem->copy_content_from_storedfile($this, $pathname);
464    }
465
466    /**
467     * Copy content of file to temporary folder and returns file path
468     *
469     * @param string $dir name of the temporary directory
470     * @param string $fileprefix prefix of temporary file.
471     * @return string|bool path of temporary file or false.
472     */
473    public function copy_content_to_temp($dir = 'files', $fileprefix = 'tempup_') {
474        $tempfile = false;
475        if (!$dir = make_temp_directory($dir)) {
476            return false;
477        }
478        if (!$tempfile = tempnam($dir, $fileprefix)) {
479            return false;
480        }
481        if (!$this->copy_content_to($tempfile)) {
482            // something went wrong
483            @unlink($tempfile);
484            return false;
485        }
486        return $tempfile;
487    }
488
489    /**
490     * List contents of archive.
491     *
492     * @param file_packer $packer file packer instance
493     * @return array of file infos
494     */
495    public function list_files(file_packer $packer) {
496        return $this->filesystem->list_files($this, $packer);
497    }
498
499    /**
500     * Returns the total size (in bytes) of the contents of an archive.
501     *
502     * @param file_packer $packer file packer instance
503     * @return int|null total size in bytes
504     */
505    public function get_total_content_size(file_packer $packer): ?int {
506        // Fetch the contents of the archive.
507        $files = $this->list_files($packer);
508
509        // Early return if the value of $files is not of type array.
510        // This can happen when the utility class is unable to open or read the contents of the archive.
511        if (!is_array($files)) {
512            return null;
513        }
514
515        return array_reduce($files, function ($contentsize, $file) {
516            return $contentsize + $file->size;
517        }, 0);
518    }
519
520    /**
521     * Extract file to given file path (real OS filesystem), existing files are overwritten.
522     *
523     * @param file_packer $packer file packer instance
524     * @param string $pathname target directory
525     * @param file_progress $progress Progress indicator callback or null if not required
526     * @return array|bool list of processed files; false if error
527     */
528    public function extract_to_pathname(file_packer $packer, $pathname,
529            file_progress $progress = null) {
530        return $this->filesystem->extract_to_pathname($this, $packer, $pathname, $progress);
531    }
532
533    /**
534     * Extract file to given file path (real OS filesystem), existing files are overwritten.
535     *
536     * @param file_packer $packer file packer instance
537     * @param int $contextid context ID
538     * @param string $component component
539     * @param string $filearea file area
540     * @param int $itemid item ID
541     * @param string $pathbase path base
542     * @param int $userid user ID
543     * @param file_progress $progress Progress indicator callback or null if not required
544     * @return array|bool list of processed files; false if error
545     */
546    public function extract_to_storage(file_packer $packer, $contextid,
547            $component, $filearea, $itemid, $pathbase, $userid = null, file_progress $progress = null) {
548
549        return $this->filesystem->extract_to_storage($this, $packer, $contextid, $component, $filearea,
550                $itemid, $pathbase, $userid, $progress);
551    }
552
553    /**
554     * Add file/directory into archive.
555     *
556     * @param file_archive $filearch file archive instance
557     * @param string $archivepath pathname in archive
558     * @return bool success
559     */
560    public function archive_file(file_archive $filearch, $archivepath) {
561        if ($this->repository) {
562            $this->sync_external_file();
563            if ($this->compare_to_string('')) {
564                // This file is not stored locally - attempt to retrieve it from the repository.
565                // This may happen if the repository deliberately does not fetch files, or if there is a failure with the sync.
566                $fileinfo = $this->repository->get_file($this->get_reference());
567                if (isset($fileinfo['path'])) {
568                    return $filearch->add_file_from_pathname($archivepath, $fileinfo['path']);
569                }
570            }
571        }
572
573        return $this->filesystem->add_storedfile_to_archive($this, $filearch, $archivepath);
574    }
575
576    /**
577     * Returns information about image,
578     * information is determined from the file content
579     *
580     * @return mixed array with width, height and mimetype; false if not an image
581     */
582    public function get_imageinfo() {
583        return $this->filesystem->get_imageinfo($this);
584    }
585
586    /**
587     * Verifies the file is a valid web image - gif, png and jpeg only.
588     *
589     * It should be ok to serve this image from server without any other security workarounds.
590     *
591     * @return bool true if file ok
592     */
593    public function is_valid_image() {
594        $mimetype = $this->get_mimetype();
595        if (!file_mimetype_in_typegroup($mimetype, 'web_image')) {
596            return false;
597        }
598        if (!$info = $this->get_imageinfo()) {
599            return false;
600        }
601        if ($info['mimetype'] !== $mimetype) {
602            return false;
603        }
604        // ok, GD likes this image
605        return true;
606    }
607
608    /**
609     * Returns parent directory, creates missing parents if needed.
610     *
611     * @return stored_file
612     */
613    public function get_parent_directory() {
614        if ($this->file_record->filepath === '/' and $this->file_record->filename === '.') {
615            //root dir does not have parent
616            return null;
617        }
618
619        if ($this->file_record->filename !== '.') {
620            return $this->fs->create_directory($this->file_record->contextid, $this->file_record->component, $this->file_record->filearea, $this->file_record->itemid, $this->file_record->filepath);
621        }
622
623        $filepath = $this->file_record->filepath;
624        $filepath = trim($filepath, '/');
625        $dirs = explode('/', $filepath);
626        array_pop($dirs);
627        $filepath = implode('/', $dirs);
628        $filepath = ($filepath === '') ? '/' : "/$filepath/";
629
630        return $this->fs->create_directory($this->file_record->contextid, $this->file_record->component, $this->file_record->filearea, $this->file_record->itemid, $filepath);
631    }
632
633    /**
634     * Set synchronised content from file.
635     *
636     * @param string $path Path to the file.
637     */
638    public function set_synchronised_content_from_file($path) {
639        $this->fs->synchronise_stored_file_from_file($this, $path, $this->file_record);
640    }
641
642    /**
643     * Set synchronised content from content.
644     *
645     * @param string $content File content.
646     */
647    public function set_synchronised_content_from_string($content) {
648        $this->fs->synchronise_stored_file_from_string($this, $content, $this->file_record);
649    }
650
651    /**
652     * Synchronize file if it is a reference and needs synchronizing
653     *
654     * Updates contenthash and filesize
655     */
656    public function sync_external_file() {
657        if (!empty($this->repository)) {
658            $this->repository->sync_reference($this);
659        }
660    }
661
662    /**
663     * Returns context id of the file
664     *
665     * @return int context id
666     */
667    public function get_contextid() {
668        return $this->file_record->contextid;
669    }
670
671    /**
672     * Returns component name - this is the owner of the areas,
673     * nothing else is allowed to read or modify the files directly!!
674     *
675     * @return string
676     */
677    public function get_component() {
678        return $this->file_record->component;
679    }
680
681    /**
682     * Returns file area name, this divides files of one component into groups with different access control.
683     * All files in one area have the same access control.
684     *
685     * @return string
686     */
687    public function get_filearea() {
688        return $this->file_record->filearea;
689    }
690
691    /**
692     * Returns returns item id of file.
693     *
694     * @return int
695     */
696    public function get_itemid() {
697        return $this->file_record->itemid;
698    }
699
700    /**
701     * Returns file path - starts and ends with /, \ are not allowed.
702     *
703     * @return string
704     */
705    public function get_filepath() {
706        return $this->file_record->filepath;
707    }
708
709    /**
710     * Returns file name or '.' in case of directories.
711     *
712     * @return string
713     */
714    public function get_filename() {
715        return $this->file_record->filename;
716    }
717
718    /**
719     * Returns id of user who created the file.
720     *
721     * @return int
722     */
723    public function get_userid() {
724        return $this->file_record->userid;
725    }
726
727    /**
728     * Returns the size of file in bytes.
729     *
730     * @return int bytes
731     */
732    public function get_filesize() {
733        $this->sync_external_file();
734        return $this->file_record->filesize;
735    }
736
737     /**
738     * Function stored_file::set_filesize() is deprecated. Please use stored_file::replace_file_with
739     *
740     * @deprecated since Moodle 2.6 MDL-42016 - please do not use this function any more.
741     * @see stored_file::replace_file_with()
742     */
743    public function set_filesize($filesize) {
744        throw new coding_exception('Function stored_file::set_filesize() can not be used any more. ' .
745            'Please use stored_file::replace_file_with()');
746    }
747
748    /**
749     * Returns mime type of file.
750     *
751     * @return string
752     */
753    public function get_mimetype() {
754        return $this->file_record->mimetype;
755    }
756
757    /**
758     * Returns unix timestamp of file creation date.
759     *
760     * @return int
761     */
762    public function get_timecreated() {
763        return $this->file_record->timecreated;
764    }
765
766    /**
767     * Returns unix timestamp of last file modification.
768     *
769     * @return int
770     */
771    public function get_timemodified() {
772        $this->sync_external_file();
773        return $this->file_record->timemodified;
774    }
775
776    /**
777     * set timemodified
778     *
779     * @param int $timemodified
780     */
781    public function set_timemodified($timemodified) {
782        $filerecord = new stdClass;
783        $filerecord->timemodified = $timemodified;
784        $this->update($filerecord);
785    }
786
787    /**
788     * Returns file status flag.
789     *
790     * @return int 0 means file OK, anything else is a problem and file can not be used
791     */
792    public function get_status() {
793        return $this->file_record->status;
794    }
795
796    /**
797     * Returns file id.
798     *
799     * @return int
800     */
801    public function get_id() {
802        return $this->file_record->id;
803    }
804
805    /**
806     * Returns sha1 hash of file content.
807     *
808     * @return string
809     */
810    public function get_contenthash() {
811        $this->sync_external_file();
812        return $this->file_record->contenthash;
813    }
814
815    /**
816     * Returns sha1 hash of all file path components sha1("contextid/component/filearea/itemid/dir/dir/filename.ext").
817     *
818     * @return string
819     */
820    public function get_pathnamehash() {
821        return $this->file_record->pathnamehash;
822    }
823
824    /**
825     * Returns the license type of the file, it is a short name referred from license table.
826     *
827     * @return string
828     */
829    public function get_license() {
830        return $this->file_record->license;
831    }
832
833    /**
834     * Set license
835     *
836     * @param string $license license
837     */
838    public function set_license($license) {
839        $filerecord = new stdClass;
840        $filerecord->license = $license;
841        $this->update($filerecord);
842    }
843
844    /**
845     * Returns the author name of the file.
846     *
847     * @return string
848     */
849    public function get_author() {
850        return $this->file_record->author;
851    }
852
853    /**
854     * Set author
855     *
856     * @param string $author
857     */
858    public function set_author($author) {
859        $filerecord = new stdClass;
860        $filerecord->author = $author;
861        $this->update($filerecord);
862    }
863
864    /**
865     * Returns the source of the file, usually it is a url.
866     *
867     * @return string
868     */
869    public function get_source() {
870        return $this->file_record->source;
871    }
872
873    /**
874     * Set license
875     *
876     * @param string $license license
877     */
878    public function set_source($source) {
879        $filerecord = new stdClass;
880        $filerecord->source = $source;
881        $this->update($filerecord);
882    }
883
884
885    /**
886     * Returns the sort order of file
887     *
888     * @return int
889     */
890    public function get_sortorder() {
891        return $this->file_record->sortorder;
892    }
893
894    /**
895     * Set file sort order
896     *
897     * @param int $sortorder
898     * @return int
899     */
900    public function set_sortorder($sortorder) {
901        $oldorder = $this->file_record->sortorder;
902        $filerecord = new stdClass;
903        $filerecord->sortorder = $sortorder;
904        $this->update($filerecord);
905        if (!$this->is_directory()) {
906            // Callback for file sort order change.
907            if ($pluginsfunction = get_plugins_with_function('after_file_sorted')) {
908                foreach ($pluginsfunction as $plugintype => $plugins) {
909                    foreach ($plugins as $pluginfunction) {
910                        $pluginfunction($this->file_record, $oldorder, $sortorder);
911                    }
912                }
913            }
914        }
915    }
916
917    /**
918     * Returns repository id
919     *
920     * @return int|null
921     */
922    public function get_repository_id() {
923        if (!empty($this->repository)) {
924            return $this->repository->id;
925        } else {
926            return null;
927        }
928    }
929
930    /**
931     * Returns repository type.
932     *
933     * @return mixed str|null the repository type or null if is not an external file
934     * @since  Moodle 3.3
935     */
936    public function get_repository_type() {
937
938        if (!empty($this->repository)) {
939            return $this->repository->get_typename();
940        } else {
941            return null;
942        }
943    }
944
945
946    /**
947     * get reference file id
948     * @return int
949     */
950    public function get_referencefileid() {
951        return $this->file_record->referencefileid;
952    }
953
954    /**
955     * Get reference last sync time
956     * @return int
957     */
958    public function get_referencelastsync() {
959        return $this->file_record->referencelastsync;
960    }
961
962    /**
963     * Function stored_file::get_referencelifetime() is deprecated as reference
964     * life time is no longer stored in DB or returned by repository. Each
965     * repository should decide by itself when to synchronise the references.
966     *
967     * @deprecated since Moodle 2.6 MDL-42016 - please do not use this function any more.
968     * @see repository::sync_reference()
969     */
970    public function get_referencelifetime() {
971        throw new coding_exception('Function stored_file::get_referencelifetime() can not be used any more. ' .
972            'See repository::sync_reference().');
973    }
974    /**
975     * Returns file reference
976     *
977     * @return string
978     */
979    public function get_reference() {
980        return $this->file_record->reference;
981    }
982
983    /**
984     * Get human readable file reference information
985     *
986     * @return string
987     */
988    public function get_reference_details() {
989        return $this->repository->get_reference_details($this->get_reference(), $this->get_status());
990    }
991
992    /**
993     * Called after reference-file has been synchronized with the repository
994     *
995     * We update contenthash, filesize and status in files table if changed
996     * and we always update lastsync in files_reference table
997     *
998     * @param null|string $contenthash if set to null contenthash is not changed
999     * @param int $filesize new size of the file
1000     * @param int $status new status of the file (0 means OK, 666 - source missing)
1001     * @param int $timemodified last time modified of the source, if known
1002     */
1003    public function set_synchronized($contenthash, $filesize, $status = 0, $timemodified = null) {
1004        if (!$this->is_external_file()) {
1005            return;
1006        }
1007        $now = time();
1008        if ($contenthash === null) {
1009            $contenthash = $this->file_record->contenthash;
1010        }
1011        if ($contenthash != $this->file_record->contenthash) {
1012            $oldcontenthash = $this->file_record->contenthash;
1013        }
1014        // this will update all entries in {files} that have the same filereference id
1015        $this->fs->update_references($this->file_record->referencefileid, $now, null, $contenthash, $filesize, $status, $timemodified);
1016        // we don't need to call update() for this object, just set the values of changed fields
1017        $this->file_record->contenthash = $contenthash;
1018        $this->file_record->filesize = $filesize;
1019        $this->file_record->status = $status;
1020        $this->file_record->referencelastsync = $now;
1021        if ($timemodified) {
1022            $this->file_record->timemodified = $timemodified;
1023        }
1024        if (isset($oldcontenthash)) {
1025            $this->filesystem->remove_file($oldcontenthash);
1026        }
1027    }
1028
1029    /**
1030     * Sets the error status for a file that could not be synchronised
1031     */
1032    public function set_missingsource() {
1033        $this->set_synchronized($this->file_record->contenthash, $this->file_record->filesize, 666);
1034    }
1035
1036    /**
1037     * Send file references
1038     *
1039     * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours)
1040     * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
1041     * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
1042     * @param array $options additional options affecting the file serving
1043     */
1044    public function send_file($lifetime, $filter, $forcedownload, $options) {
1045        $this->repository->send_file($this, $lifetime, $filter, $forcedownload, $options);
1046    }
1047
1048    /**
1049     * Imports the contents of an external file into moodle filepool.
1050     *
1051     * @throws moodle_exception if file could not be downloaded or is too big
1052     * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
1053     */
1054    public function import_external_file_contents($maxbytes = 0) {
1055        if ($this->repository) {
1056            $this->repository->import_external_file_contents($this, $maxbytes);
1057        }
1058    }
1059
1060    /**
1061     * Gets a file relative to this file in the repository and sends it to the browser.
1062     * Checks the function repository::supports_relative_file() to make sure it can be used.
1063     *
1064     * @param string $relativepath the relative path to the file we are trying to access
1065     */
1066    public function send_relative_file($relativepath) {
1067        if ($this->repository && $this->repository->supports_relative_file()) {
1068            $relativepath = clean_param($relativepath, PARAM_PATH);
1069            $this->repository->send_relative_file($this, $relativepath);
1070        } else {
1071            send_file_not_found();
1072        }
1073    }
1074
1075    /**
1076     * Generates a thumbnail for this stored_file.
1077     *
1078     * If the GD library has at least version 2 and PNG support is available, the returned data
1079     * is the content of a transparent PNG file containing the thumbnail. Otherwise, the function
1080     * returns contents of a JPEG file with black background containing the thumbnail.
1081     *
1082     * @param   int $width the width of the requested thumbnail
1083     * @param   int $height the height of the requested thumbnail
1084     * @return  string|bool false if a problem occurs, the thumbnail image data otherwise
1085     */
1086    public function generate_image_thumbnail($width, $height) {
1087        global $CFG;
1088        require_once($CFG->libdir . '/gdlib.php');
1089
1090        if (empty($width) or empty($height)) {
1091            return false;
1092        }
1093
1094        $content = $this->get_content();
1095
1096        // Fetch the image information for this image.
1097        $imageinfo = @getimagesizefromstring($content);
1098        if (empty($imageinfo)) {
1099            return false;
1100        }
1101
1102        // Create a new image from the file.
1103        $original = @imagecreatefromstring($content);
1104
1105        // Generate the thumbnail.
1106        return generate_image_thumbnail_from_image($original, $imageinfo, $width, $height);
1107    }
1108
1109    /**
1110     * Generate a resized image for this stored_file.
1111     *
1112     * @param int|null $width The desired width, or null to only use the height.
1113     * @param int|null $height The desired height, or null to only use the width.
1114     * @return string|false False when a problem occurs, else the image data.
1115     */
1116    public function resize_image($width, $height) {
1117        global $CFG;
1118        require_once($CFG->libdir . '/gdlib.php');
1119
1120        $content = $this->get_content();
1121
1122        // Fetch the image information for this image.
1123        $imageinfo = @getimagesizefromstring($content);
1124        if (empty($imageinfo)) {
1125            return false;
1126        }
1127
1128        // Create a new image from the file.
1129        $original = @imagecreatefromstring($content);
1130
1131        // Generate the resized image.
1132        return resize_image_from_image($original, $imageinfo, $width, $height);
1133    }
1134
1135    /**
1136     * Check whether the supplied file is the same as this file.
1137     *
1138     * @param   string $path The path to the file on disk
1139     * @return  boolean
1140     */
1141    public function compare_to_path($path) {
1142        return $this->get_contenthash() === file_storage::hash_from_path($path);
1143    }
1144
1145    /**
1146     * Check whether the supplied content is the same as this file.
1147     *
1148     * @param   string $content The file content
1149     * @return  boolean
1150     */
1151    public function compare_to_string($content) {
1152        return $this->get_contenthash() === file_storage::hash_from_string($content);
1153    }
1154
1155    /**
1156     * Generate a rotated image for this stored_file based on exif information.
1157     *
1158     * @return array|false False when a problem occurs, else the image data and image size.
1159     * @since Moodle 3.8
1160     */
1161    public function rotate_image() {
1162        $content = $this->get_content();
1163        $mimetype = $this->get_mimetype();
1164
1165        if ($mimetype === "image/jpeg" && function_exists("exif_read_data")) {
1166            $exif = @exif_read_data("data://image/jpeg;base64," . base64_encode($content));
1167            if (isset($exif['ExifImageWidth']) && isset($exif['ExifImageLength']) && isset($exif['Orientation'])) {
1168                $rotation = [
1169                    3 => -180,
1170                    6 => -90,
1171                    8 => -270,
1172                ];
1173                $orientation = $exif['Orientation'];
1174                if ($orientation !== 1) {
1175                    $source = @imagecreatefromstring($content);
1176                    $data = @imagerotate($source, $rotation[$orientation], 0);
1177                    if (!empty($data)) {
1178                        if ($orientation == 1 || $orientation == 3) {
1179                            $size = [
1180                                'width' => $exif["ExifImageWidth"],
1181                                'height' => $exif["ExifImageLength"],
1182                            ];
1183                        } else {
1184                            $size = [
1185                                'height' => $exif["ExifImageWidth"],
1186                                'width' => $exif["ExifImageLength"],
1187                            ];
1188                        }
1189                        imagedestroy($source);
1190                        return [$data, $size];
1191                    }
1192                }
1193            }
1194        }
1195        return [false, false];
1196    }
1197}
1198