bfcf25466342b022c1a2b6c0ab04cacea0875f44
[moodle.git] / repository / filesystem / lib.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/>.
17 /**
18  * This plugin is used to access files on server file system
19  *
20  * @since Moodle 2.0
21  * @package    repository_filesystem
22  * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 require_once($CFG->dirroot . '/repository/lib.php');
26 require_once($CFG->libdir . '/filelib.php');
28 /**
29  * repository_filesystem class
30  *
31  * Create a repository from your local filesystem
32  * *NOTE* for security issue, we use a fixed repository path
33  * which is %moodledata%/repository
34  *
35  * @package    repository
36  * @copyright  2009 Dongsheng Cai {@link http://dongsheng.org}
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class repository_filesystem extends repository {
41     /**
42      * The subdirectory of the instance.
43      *
44      * @var string
45      */
46     protected $subdir;
48     /**
49      * Constructor
50      *
51      * @param int $repositoryid repository ID
52      * @param int $context context ID
53      * @param array $options
54      */
55     public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
56         parent::__construct($repositoryid, $context, $options);
57         $this->subdir = $this->get_option('fs_path');
58     }
60     /**
61      * Get the list of files and directories in that repository.
62      *
63      * @param string $fullpath Path to explore. This is assembled by {@link self::build_node_path()}.
64      * @param string $page Page number.
65      * @return array List of files and folders.
66      */
67     public function get_listing($fullpath = '', $page = '') {
68         global $OUTPUT;
70         $list = array(
71             'list' => array(),
72             'manage' => false,
73             'dynload' => true,
74             'nologin' => true,
75             'path' => array()
76         );
78         // We analyse the path to extract what to browse.
79         $fullpath = empty($fullpath) ? $this->build_node_path('root') : $fullpath;
80         $trail = explode('|', $fullpath);
81         $trail = array_pop($trail);
82         list($mode, $path, $unused) = $this->explode_node_path($trail);
84         // Is that a search?
85         if ($mode === 'search') {
86             return $this->search($path, $page);
87         }
89         // Cleaning up the requested path.
90         $path = trim($path, '/');
91         if (!$this->is_in_repository($path)) {
92             // In case of doubt on the path, reset to default.
93             $path = '';
94         }
95         $rootpath = $this->get_rootpath();
96         $abspath = rtrim($rootpath . $path, '/') . '/';
98         // Retrieve list of files and directories and sort them.
99         $fileslist = array();
100         $dirslist = array();
101         if ($dh = opendir($abspath)) {
102             while (($file = readdir($dh)) != false) {
103                 if ($file != '.' and $file != '..') {
104                     if (is_file($abspath . $file)) {
105                         $fileslist[] = $file;
106                     } else {
107                         $dirslist[] = $file;
108                     }
109                 }
110             }
111         }
112         core_collator::asort($fileslist, core_collator::SORT_NATURAL);
113         core_collator::asort($dirslist, core_collator::SORT_NATURAL);
115         // Fill the results.
116         foreach ($dirslist as $file) {
117             $list['list'][] = $this->build_node($rootpath, $path, $file, true, $fullpath);
118         }
119         foreach ($fileslist as $file) {
120             $list['list'][] = $this->build_node($rootpath, $path, $file, false, $fullpath);
121         }
123         $list['path'] = $this->build_breadcrumb($fullpath);
124         $list['list'] = array_filter($list['list'], array($this, 'filter'));
126         return $list;
127     }
129     /**
130      * Search files in repository.
131      *
132      * This search works by walking through the directories returning the files that match. Once
133      * the limit of files is reached the walk stops. Whenever more files are requested, the walk
134      * starts from the beginning until it reaches an additional set of files to return.
135      *
136      * @param string $query The query string.
137      * @param int $page The page number.
138      * @return mixed
139      */
140     public function search($query, $page = 1) {
141         global $OUTPUT, $SESSION;
143         $query = core_text::strtolower($query);
144         $remainingdirs = 1000;
145         $remainingobjects = 5000;
146         $perpage = 50;
148         // Because the repository API is weird, the first page is 0, but it should be 1.
149         if (!$page) {
150             $page = 1;
151         }
153         // Initialise the session variable in which we store the search related things.
154         if (!isset($SESSION->repository_filesystem_search)) {
155             $SESSION->repository_filesystem_search = array();
156         }
158         // Restore, or initialise the session search variables.
159         if ($page <= 1) {
160             $SESSION->repository_filesystem_search['query'] = $query;
161             $SESSION->repository_filesystem_search['from'] = 0;
162             $from = 0;
163         } else {
164             // Yes, the repository does not send the query again...
165             $query = $SESSION->repository_filesystem_search['query'];
166             $from = (int) $SESSION->repository_filesystem_search['from'];
167         }
168         $limit = $from + $perpage;
169         $searchpath = $this->build_node_path('search', $query);
171         // Pre-search initialisation.
172         $rootpath = $this->get_rootpath();
173         $found = 0;
174         $toexplore = array('');
176         // Retrieve list of matching files and directories.
177         $matches = array();
178         while (($path = array_shift($toexplore)) !== null) {
179             $remainingdirs--;
181             if ($objects = scandir($rootpath . $path)) {
182                 foreach ($objects as $object) {
183                     $objectabspath = $rootpath . $path . $object;
184                     if ($object == '.' || $object == '..') {
185                         continue;
186                     }
188                     $remainingobjects--;
189                     $isdir = is_dir($objectabspath);
191                     // It is a match!
192                     if (strpos(core_text::strtolower($object), $query) !== false) {
193                         $found++;
194                         $matches[] = array($path, $object, $isdir);
196                         // That's enough, no need to find more.
197                         if ($found >= $limit) {
198                             break 2;
199                         }
200                     }
202                     // I've seen enough files, I give up!
203                     if ($remainingobjects <= 0) {
204                         break 2;
205                     }
207                     // Add the directory to things to explore later.
208                     if ($isdir) {
209                         $toexplore[] = $path . trim($object, '/') . '/';
210                     }
211                 }
212             }
214             if ($remainingdirs <= 0) {
215                 break;
216             }
217         }
219         // Extract the results from all the matches.
220         $matches = array_slice($matches, $from, $perpage);
222         // If we didn't reach our limits of browsing, and we appear to still have files to find.
223         if ($remainingdirs > 0 && $remainingobjects > 0 && count($matches) >= $perpage) {
224             $SESSION->repository_filesystem_search['from'] = $limit;
225             $pages = -1;
227         // We reached the end of the repository, or our limits.
228         } else {
229             $SESSION->repository_filesystem_search['from'] = 0;
230             $pages = 0;
231         }
233         // Organise the nodes.
234         $results = array();
235         foreach ($matches as $match) {
236             list($path, $name, $isdir) = $match;
237             $results[] = $this->build_node($rootpath, $path, $name, $isdir, $searchpath);
238         }
240         $list = array();
241         $list['list'] = array_filter($results, array($this, 'filter'));
242         $list['dynload'] = true;
243         $list['nologin'] = true;
244         $list['page'] = $page;
245         $list['pages'] = $pages;
246         $list['path'] = $this->build_breadcrumb($searchpath);
248         return $list;
249     }
251     /**
252      * Build the breadcrumb from a full path.
253      *
254      * @param string $path A path generated by {@link self::build_node_path()}.
255      * @return array
256      */
257     protected function build_breadcrumb($path) {
258         $breadcrumb = array(array(
259             'name' => get_string('root', 'repository_filesystem'),
260             'path' => $this->build_node_path('root')
261         ));
263         $crumbs = explode('|', $path);
264         $trail = '';
266         foreach ($crumbs as $crumb) {
267             list($mode, $nodepath, $display) = $this->explode_node_path($crumb);
268             switch ($mode) {
269                 case 'search':
270                     $breadcrumb[] = array(
271                         'name' => get_string('searchresults', 'repository_filesystem'),
272                         'path' => $this->build_node_path($mode, $nodepath, $display, $trail),
273                     );
274                     break;
276                 case 'browse':
277                     $breadcrumb[] = array(
278                         'name' => $display,
279                         'path' => $this->build_node_path($mode, $nodepath, $display, $trail),
280                     );
281                     break;
282             }
284             $lastcrumb = end($breadcrumb);
285             $trail = $lastcrumb['path'];
286         }
288         return $breadcrumb;
289     }
291     /**
292      * Build a file or directory node.
293      *
294      * @param string $rootpath The absolute path to the repository.
295      * @param string $path The relative path of the object
296      * @param string $name The name of the object
297      * @param string $isdir Is the object a directory?
298      * @param string $rootnodepath The node leading to this node (for breadcrumb).
299      * @return array
300      */
301     protected function build_node($rootpath, $path, $name, $isdir, $rootnodepath) {
302         global $OUTPUT;
304         $relpath = trim($path, '/') . '/' . $name;
305         $abspath = $rootpath . $relpath;
306         $node = array(
307             'title' => $name,
308             'datecreated' => filectime($abspath),
309             'datemodified' => filemtime($abspath),
310         );
312         if ($isdir) {
313             $node['children'] = array();
314             $node['thumbnail'] = $OUTPUT->pix_url(file_folder_icon(90))->out(false);
315             $node['path'] = $this->build_node_path('browse', $relpath, $name, $rootnodepath);
317         } else {
318             $node['source'] = $relpath;
319             $node['size'] = filesize($abspath);
320             $node['thumbnail'] = $OUTPUT->pix_url(file_extension_icon($name, 90))->out(false);
321             $node['icon'] = $OUTPUT->pix_url(file_extension_icon($name, 24))->out(false);
322             $node['path'] = $relpath;
324             if (file_extension_in_typegroup($name, 'image') && ($imageinfo = @getimagesize($abspath))) {
325                 // This means it is an image and we can return dimensions and try to generate thumbnail/icon.
326                 $token = $node['datemodified'] . $node['size']; // To prevent caching by browser.
327                 $node['realthumbnail'] = $this->get_thumbnail_url($relpath, 'thumb', $token)->out(false);
328                 $node['realicon'] = $this->get_thumbnail_url($relpath, 'icon', $token)->out(false);
329                 $node['image_width'] = $imageinfo[0];
330                 $node['image_height'] = $imageinfo[1];
331             }
332         }
334         return $node;
335     }
337     /**
338      * Build the path to a browsable node.
339      *
340      * @param string $mode The type of browse mode.
341      * @param string $realpath The path, or similar.
342      * @param string $display The way to display the node.
343      * @param string $root The path preceding this node.
344      * @return string
345      */
346     protected function build_node_path($mode, $realpath = '', $display = '', $root = '') {
347         $path = $mode . ':' . base64_encode($realpath) . ':' . base64_encode($display);
348         if (!empty($root)) {
349             $path = $root . '|' . $path;
350         }
351         return $path;
352     }
354     /**
355      * Extract information from a node path.
356      *
357      * Note, this should not include preceding paths.
358      *
359      * @param string $path The path of the node.
360      * @return array Contains the mode, the relative path, and the display text.
361      */
362     protected function explode_node_path($path) {
363         list($mode, $realpath, $display) = explode(':', $path);
364         return array(
365             $mode,
366             base64_decode($realpath),
367             base64_decode($display)
368         );
369     }
371     /**
372      * To check whether the user is logged in.
373      *
374      * @return bool
375      */
376     public function check_login() {
377         return true;
378     }
380     /**
381      * Show the login screen, if required.
382      *
383      * @return string
384      */
385     public function print_login() {
386         return true;
387     }
389     /**
390      * Is it possible to do a global search?
391      *
392      * @return bool
393      */
394     public function global_search() {
395         return false;
396     }
398     /**
399      * Return file path.
400      * @return array
401      */
402     public function get_file($file, $title = '') {
403         global $CFG;
404         $file = ltrim($file, '/');
405         if (!$this->is_in_repository($file)) {
406             throw new repository_exception('Invalid file requested.');
407         }
408         $file = $this->get_rootpath() . $file;
410         // This is a hack to prevent move_to_file deleting files in local repository.
411         $CFG->repository_no_delete = true;
412         return array('path' => $file, 'url' => '');
413     }
415     /**
416      * Return the source information
417      *
418      * @param stdClass $filepath
419      * @return string|null
420      */
421     public function get_file_source_info($filepath) {
422         return $filepath;
423     }
425     /**
426      * Logout from repository instance
427      *
428      * @return string
429      */
430     public function logout() {
431         return true;
432     }
434     /**
435      * Return names of the instance options.
436      *
437      * @return array
438      */
439     public static function get_instance_option_names() {
440         return array('fs_path', 'relativefiles');
441     }
443     /**
444      * Save settings for repository instance
445      *
446      * @param array $options settings
447      * @return bool
448      */
449     public function set_option($options = array()) {
450         $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH);
451         $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT);
452         $ret = parent::set_option($options);
453         return $ret;
454     }
456     /**
457      * Edit/Create Instance Settings Moodle form
458      *
459      * @param moodleform $mform Moodle form (passed by reference)
460      */
461     public static function instance_config_form($mform) {
462         global $CFG;
463         if (has_capability('moodle/site:config', context_system::instance())) {
464             $path = $CFG->dataroot . '/repository/';
465             if (!is_dir($path)) {
466                 mkdir($path, $CFG->directorypermissions, true);
467             }
468             if ($handle = opendir($path)) {
469                 $fieldname = get_string('path', 'repository_filesystem');
470                 $choices = array();
471                 while (false !== ($file = readdir($handle))) {
472                     if (is_dir($path . $file) && $file != '.' && $file != '..') {
473                         $choices[$file] = $file;
474                         $fieldname = '';
475                     }
476                 }
477                 if (empty($choices)) {
478                     $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path));
479                     $mform->addElement('hidden', 'fs_path', '');
480                     $mform->setType('fs_path', PARAM_PATH);
481                 } else {
482                     $mform->addElement('select', 'fs_path', $fieldname, $choices);
483                     $mform->addElement('static', null, '',  get_string('information', 'repository_filesystem', $path));
484                 }
485                 closedir($handle);
486             }
487             $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'),
488                 get_string('relativefiles_desc', 'repository_filesystem'));
489             $mform->setType('relativefiles', PARAM_INT);
491         } else {
492             $mform->addElement('static', null, '',  get_string('nopermissions', 'error', get_string('configplugin',
493                 'repository_filesystem')));
494             return false;
495         }
496     }
498     /**
499      * Create an instance for this plug-in
500      *
501      * @static
502      * @param string $type the type of the repository
503      * @param int $userid the user id
504      * @param stdClass $context the context
505      * @param array $params the options for this instance
506      * @param int $readonly whether to create it readonly or not (defaults to not)
507      * @return mixed
508      */
509     public static function create($type, $userid, $context, $params, $readonly=0) {
510         if (has_capability('moodle/site:config', context_system::instance())) {
511             return parent::create($type, $userid, $context, $params, $readonly);
512         } else {
513             require_capability('moodle/site:config', context_system::instance());
514             return false;
515         }
516     }
518     /**
519      * Validate repository plugin instance form
520      *
521      * @param moodleform $mform moodle form
522      * @param array $data form data
523      * @param array $errors errors
524      * @return array errors
525      */
526     public static function instance_form_validation($mform, $data, $errors) {
527         $fspath = clean_param(trim($data['fs_path'], '/'), PARAM_PATH);
528         if (empty($fspath) && !is_numeric($fspath)) {
529             $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path');
530         }
531         return $errors;
532     }
534     /**
535      * User cannot use the external link to dropbox
536      *
537      * @return int
538      */
539     public function supported_returntypes() {
540         return FILE_INTERNAL | FILE_REFERENCE;
541     }
543     /**
544      * Return human readable reference information
545      *
546      * @param string $reference value of DB field files_reference.reference
547      * @param int $filestatus status of the file, 0 - ok, 666 - source missing
548      * @return string
549      */
550     public function get_reference_details($reference, $filestatus = 0) {
551         $details = $this->get_name().': '.$reference;
552         if ($filestatus) {
553             return get_string('lostsource', 'repository', $details);
554         } else {
555             return $details;
556         }
557     }
559     public function sync_reference(stored_file $file) {
560         if ($file->get_referencelastsync() + 60 > time()) {
561             // Does not cost us much to synchronise within our own filesystem, check every 1 minute.
562             return false;
563         }
564         static $issyncing = false;
565         if ($issyncing) {
566             // Avoid infinite recursion when calling $file->get_filesize() and get_contenthash().
567             return false;
568         }
569         $filepath = $this->get_rootpath() . ltrim($file->get_reference(), '/');
570         if ($this->is_in_repository($file->get_reference()) && file_exists($filepath) && is_readable($filepath)) {
571             $fs = get_file_storage();
572             $issyncing = true;
573             if (file_extension_in_typegroup($filepath, 'web_image')) {
574                 $contenthash = sha1_file($filepath);
575                 if ($file->get_contenthash() == $contenthash) {
576                     // File did not change since the last synchronisation.
577                     $filesize = filesize($filepath);
578                 } else {
579                     // Copy file into moodle filepool (used to generate an image thumbnail).
580                     list($contenthash, $filesize, $newfile) = $fs->add_file_to_pool($filepath);
581                 }
582             } else {
583                 // Update only file size so file will NOT be copied into moodle filepool.
584                 $emptyfile = $contenthash = sha1('');
585                 $currentcontenthash = $file->get_contenthash();
586                 if ($currentcontenthash !== $emptyfile && $currentcontenthash === sha1_file($filepath)) {
587                     // File content was synchronised and has not changed since then, leave it.
588                     $contenthash = null;
589                 }
590                 $filesize = filesize($filepath);
591             }
592             $issyncing = false;
593             $modified = filemtime($filepath);
594             $file->set_synchronized($contenthash, $filesize, 0, $modified);
595         } else {
596             $file->set_missingsource();
597         }
598         return true;
599     }
601     /**
602      * Repository method to serve the referenced file
603      *
604      * @see send_stored_file
605      *
606      * @param stored_file $storedfile the file that contains the reference
607      * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
608      * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
609      * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
610      * @param array $options additional options affecting the file serving
611      */
612     public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
613         $reference = $storedfile->get_reference();
614         $file = $this->get_rootpath() . ltrim($reference, '/');
615         if ($this->is_in_repository($reference) && is_readable($file)) {
616             $filename = $storedfile->get_filename();
617             if ($options && isset($options['filename'])) {
618                 $filename = $options['filename'];
619             }
620             $dontdie = ($options && isset($options['dontdie']));
621             send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie);
622         } else {
623             send_file_not_found();
624         }
625     }
627     /**
628      * Is this repository accessing private data?
629      *
630      * @return bool
631      */
632     public function contains_private_data() {
633         return false;
634     }
636     /**
637      * Return the rootpath of this repository instance.
638      *
639      * Trim() is a necessary step to ensure that the subdirectory is not '/'.
640      *
641      * @return string path
642      * @throws repository_exception If the subdir is unsafe, or invalid.
643      */
644     public function get_rootpath() {
645         global $CFG;
646         $subdir = clean_param(trim($this->subdir, '/'), PARAM_PATH);
647         $path = $CFG->dataroot . '/repository/' . $this->subdir . '/';
648         if ((empty($this->subdir) && !is_numeric($this->subdir)) || $subdir != $this->subdir || !is_dir($path)) {
649             throw new repository_exception('The instance is not properly configured, invalid path.');
650         }
651         return $path;
652     }
654     /**
655      * Checks if $path is part of this repository.
656      *
657      * Try to prevent $path hacks such as ../ .
658      *
659      * We do not use clean_param(, PARAM_PATH) here because it also trims down some
660      * characters that are allowed, like < > ' . But we do ensure that the directory
661      * is safe by checking that it starts with $rootpath.
662      *
663      * @param string $path relative path to a file or directory in the repo.
664      * @return boolean false when not.
665      */
666     protected function is_in_repository($path) {
667         $rootpath = $this->get_rootpath();
668         if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) {
669             return false;
670         }
671         return true;
672     }
674     /**
675      * Returns url of thumbnail file.
676      *
677      * @param string $filepath current path in repository (dir and filename)
678      * @param string $thumbsize 'thumb' or 'icon'
679      * @param string $token identifier of the file contents - to prevent browser from caching changed file
680      * @return moodle_url
681      */
682     protected function get_thumbnail_url($filepath, $thumbsize, $token) {
683         return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id,
684                 '/' . trim($filepath, '/') . '/', $token);
685     }
687     /**
688      * Returns the stored thumbnail file, generates it if not present.
689      *
690      * @param string $filepath current path in repository (dir and filename)
691      * @param string $thumbsize 'thumb' or 'icon'
692      * @return null|stored_file
693      */
694     public function get_thumbnail($filepath, $thumbsize) {
695         global $CFG;
697         $filepath = trim($filepath, '/');
698         $origfile = $this->get_rootpath() . $filepath;
699         // As thumbnail filename we use original file content hash.
700         if (!$this->is_in_repository($filepath) || !($filecontents = @file_get_contents($origfile))) {
701             // File is not found or is not readable.
702             return null;
703         }
704         $filename = sha1($filecontents);
706         // Try to get generated thumbnail for this file.
707         $fs = get_file_storage();
708         if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/',
709                 $filename))) {
710             // Thumbnail not found . Generate and store thumbnail.
711             require_once($CFG->libdir . '/gdlib.php');
712             if ($thumbsize === 'thumb') {
713                 $size = 90;
714             } else {
715                 $size = 24;
716             }
717             if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) {
718                 // Generation failed.
719                 return null;
720             }
721             $record = array(
722                 'contextid' => SYSCONTEXTID,
723                 'component' => 'repository_filesystem',
724                 'filearea' => $thumbsize,
725                 'itemid' => $this->id,
726                 'filepath' => '/' . $filepath . '/',
727                 'filename' => $filename,
728             );
729             $file = $fs->create_file_from_string($record, $data);
730         }
731         return $file;
732     }
734     /**
735      * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files.
736      *
737      * @param stored_file[] $storedfiles
738      */
739     public function remove_obsolete_thumbnails($storedfiles) {
740         // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository).
741         $files = array();
742         foreach ($storedfiles as $file) {
743             if (!isset($files[$file->get_filepath()])) {
744                 $files[$file->get_filepath()] = array();
745             }
746             $files[$file->get_filepath()][] = $file;
747         }
749         // Loop through all files and make sure the original exists and has the same contenthash.
750         $deletedcount = 0;
751         foreach ($files as $filepath => $filesinpath) {
752             if ($filecontents = @file_get_contents($this->get_rootpath() . trim($filepath, '/'))) {
753                 // The 'filename' in Moodle file storage is contenthash of the file in filesystem repository.
754                 $filename = sha1($filecontents);
755                 foreach ($filesinpath as $file) {
756                     if ($file->get_filename() !== $filename && $file->get_filename() !== '.') {
757                         // Contenthash does not match, this is an old thumbnail.
758                         $deletedcount++;
759                         $file->delete();
760                     }
761                 }
762             } else {
763                 // Thumbnail exist but file not.
764                 foreach ($filesinpath as $file) {
765                     if ($file->get_filename() !== '.') {
766                         $deletedcount++;
767                     }
768                     $file->delete();
769                 }
770             }
771         }
772         if ($deletedcount) {
773             mtrace(" instance {$this->id}: deleted $deletedcount thumbnails");
774         }
775     }
777     /**
778      *  Gets a file relative to this file in the repository and sends it to the browser.
779      *
780      * @param stored_file $mainfile The main file we are trying to access relative files for.
781      * @param string $relativepath the relative path to the file we are trying to access.
782      */
783     public function send_relative_file(stored_file $mainfile, $relativepath) {
784         global $CFG;
785         // Check if this repository is allowed to use relative linking.
786         $allowlinks = $this->supports_relative_file();
787         if (!empty($allowlinks)) {
788             // Get path to the mainfile.
789             $mainfilepath = $mainfile->get_source();
791             // Strip out filename from the path.
792             $filename = $mainfile->get_filename();
793             $basepath = strstr($mainfilepath, $filename, true);
795             $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath);
797             // Sanity check to make sure this path is inside this repository and the file exists.
798             if (strpos($fullrelativefilepath, realpath($this->get_rootpath())) === 0 && file_exists($fullrelativefilepath)) {
799                 send_file($fullrelativefilepath, basename($relativepath), null, 0);
800             }
801         }
802         send_file_not_found();
803     }
805     /**
806      * helper function to check if the repository supports send_relative_file.
807      *
808      * @return true|false
809      */
810     public function supports_relative_file() {
811         return $this->get_option('relativefiles');
812     }
814     /**
815      * Helper funtion to indicate if this repository uses post requests for uploading files.
816      *
817      * Files are copied from the filesystem so don't rely on POST requests.
818      *
819      * @return bool
820      */
821     public function uses_post_requests() {
822         return false;
823     }
826 /**
827  * Generates and sends the thumbnail for an image in filesystem.
828  *
829  * @param stdClass $course course object
830  * @param stdClass $cm course module object
831  * @param stdClass $context context object
832  * @param string $filearea file area
833  * @param array $args extra arguments
834  * @param bool $forcedownload whether or not force download
835  * @param array $options additional options affecting the file serving
836  * @return bool
837  */
838 function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
839     global $OUTPUT, $CFG;
840     // Allowed filearea is either thumb or icon - size of the thumbnail.
841     if ($filearea !== 'thumb' && $filearea !== 'icon') {
842         return false;
843     }
845     // As itemid we pass repository instance id.
846     $itemid = array_shift($args);
847     // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed).
848     array_pop($args);
849     // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository.
850     $filepath = implode('/', $args);
852     // Make sure file exists in the repository and is accessible.
853     $repo = repository::get_repository_by_id($itemid, $context);
854     $repo->check_capability();
855     // Find stored or generated thumbnail.
856     if (!($file = $repo->get_thumbnail($filepath, $filearea))) {
857         // Generation failed, redirect to default icon for file extension.
858         redirect($OUTPUT->pix_url(file_extension_icon($file, 90)));
859     }
860     // The thumbnails should not be changing much, but maybe the default lifetime is too long.
861     $lifetime = $CFG->filelifetime;
862     if ($lifetime > 60*10) {
863         $lifetime = 60*10;
864     }
865     send_stored_file($file, $lifetime, 0, $forcedownload, $options);
868 /**
869  * Cron callback for repository_filesystem. Deletes the thumbnails for deleted or changed files.
870  */
871 function repository_filesystem_cron() {
872     $fs = get_file_storage();
873     // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id).
874     $allfiles = array_merge(
875             $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'),
876             $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon')
877     );
878     $filesbyitem = array();
879     foreach ($allfiles as $file) {
880         if (!isset($filesbyitem[$file->get_itemid()])) {
881             $filesbyitem[$file->get_itemid()] = array();
882         }
883         $filesbyitem[$file->get_itemid()][] = $file;
884     }
885     // Find all instances of repository_filesystem.
886     $instances = repository::get_instances(array('type' => 'filesystem'));
887     // Loop through all itemids of generated thumbnails.
888     foreach ($filesbyitem as $itemid => $files) {
889         if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) {
890             // Instance was deleted.
891             $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid);
892             $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid);
893             mtrace(" instance $itemid does not exist: deleted all thumbnails");
894         } else {
895             // Instance has some generated thumbnails, check that they are not outdated.
896             $instances[$itemid]->remove_obsolete_thumbnails($files);
897         }
898     }