Merge branch 'MDL-60301-master' of git://github.com/ankitagarwal/moodle
[moodle.git] / repository / filesystem / lib.php
blobce8502fe9e9c545fb7116a63b2b1c15d5a40aa49
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.
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
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
25 require_once($CFG->dirroot . '/repository/lib.php');
26 require_once($CFG->libdir . '/filelib.php');
28 /**
29 * repository_filesystem class
31 * Create a repository from your local filesystem
32 * *NOTE* for security issue, we use a fixed repository path
33 * which is %moodledata%/repository
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
39 class repository_filesystem extends repository {
41 /**
42 * The subdirectory of the instance.
44 * @var string
46 protected $subdir;
48 /**
49 * Constructor
51 * @param int $repositoryid repository ID
52 * @param int $context context ID
53 * @param array $options
55 public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
56 parent::__construct($repositoryid, $context, $options);
57 $this->subdir = $this->get_option('fs_path');
60 /**
61 * Get the list of files and directories in that repository.
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.
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()
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);
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 = '';
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;
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);
119 foreach ($fileslist as $file) {
120 $list['list'][] = $this->build_node($rootpath, $path, $file, false, $fullpath);
123 $list['path'] = $this->build_breadcrumb($fullpath);
124 $list['list'] = array_filter($list['list'], array($this, 'filter'));
126 return $list;
130 * Search files in repository.
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.
136 * @param string $query The query string.
137 * @param int $page The page number.
138 * @return mixed
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;
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();
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'];
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;
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;
202 // I've seen enough files, I give up!
203 if ($remainingobjects <= 0) {
204 break 2;
207 // Add the directory to things to explore later.
208 if ($isdir) {
209 $toexplore[] = $path . trim($object, '/') . '/';
214 if ($remainingdirs <= 0) {
215 break;
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;
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);
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;
252 * Build the breadcrumb from a full path.
254 * @param string $path A path generated by {@link self::build_node_path()}.
255 * @return array
257 protected function build_breadcrumb($path) {
258 $breadcrumb = array(array(
259 'name' => get_string('root', 'repository_filesystem'),
260 'path' => $this->build_node_path('root')
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),
274 break;
276 case 'browse':
277 $breadcrumb[] = array(
278 'name' => $display,
279 'path' => $this->build_node_path($mode, $nodepath, $display, $trail),
281 break;
284 $lastcrumb = end($breadcrumb);
285 $trail = $lastcrumb['path'];
288 return $breadcrumb;
292 * Build a file or directory node.
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
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),
312 if ($isdir) {
313 $node['children'] = array();
314 $node['thumbnail'] = $OUTPUT->image_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->image_url(file_extension_icon($name, 90))->out(false);
321 $node['icon'] = $OUTPUT->image_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];
334 return $node;
338 * Build the path to a browsable node.
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
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;
351 return $path;
355 * Extract information from a node path.
357 * Note, this should not include preceding paths.
359 * @param string $path The path of the node.
360 * @return array Contains the mode, the relative path, and the display text.
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)
372 * To check whether the user is logged in.
374 * @return bool
376 public function check_login() {
377 return true;
381 * Show the login screen, if required.
383 * @return string
385 public function print_login() {
386 return true;
390 * Is it possible to do a global search?
392 * @return bool
394 public function global_search() {
395 return false;
399 * Return file path.
400 * @return array
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.');
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' => '');
416 * Return the source information
418 * @param stdClass $filepath
419 * @return string|null
421 public function get_file_source_info($filepath) {
422 return $filepath;
426 * Logout from repository instance
428 * @return string
430 public function logout() {
431 return true;
435 * Return names of the instance options.
437 * @return array
439 public static function get_instance_option_names() {
440 return array('fs_path', 'relativefiles');
444 * Save settings for repository instance
446 * @param array $options settings
447 * @return bool
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;
457 * Edit/Create Instance Settings Moodle form
459 * @param moodleform $mform Moodle form (passed by reference)
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);
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 = '';
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));
485 closedir($handle);
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;
499 * Create an instance for this plug-in
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
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;
519 * Validate repository plugin instance form
521 * @param moodleform $mform moodle form
522 * @param array $data form data
523 * @param array $errors errors
524 * @return array errors
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');
531 return $errors;
535 * User cannot use the external link to dropbox
537 * @return int
539 public function supported_returntypes() {
540 return FILE_INTERNAL | FILE_REFERENCE;
544 * Return human readable reference information
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
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;
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;
564 static $issyncing = false;
565 if ($issyncing) {
566 // Avoid infinite recursion when calling $file->get_filesize() and get_contenthash().
567 return false;
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 = file_storage::hash_from_path($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 $file->set_timemodified(filemtime($filepath));
581 $file->set_synchronised_content_from_file($filepath);
582 return true;
584 } else {
585 // Update only file size so file will NOT be copied into moodle filepool.
586 if ($file->compare_to_string('') || !$file->compare_to_path($filepath)) {
587 // File is not synchronized or the file has changed.
588 $contenthash = file_storage::hash_from_string('');
589 } else {
590 // File content was synchronised and has not changed since then, leave it.
591 $contenthash = null;
593 $filesize = filesize($filepath);
595 $issyncing = false;
596 $modified = filemtime($filepath);
597 $file->set_synchronized($contenthash, $filesize, 0, $modified);
598 } else {
599 $file->set_missingsource();
601 return true;
605 * Repository method to serve the referenced file
607 * @see send_stored_file
609 * @param stored_file $storedfile the file that contains the reference
610 * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
611 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
612 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
613 * @param array $options additional options affecting the file serving
615 public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
616 $reference = $storedfile->get_reference();
617 $file = $this->get_rootpath() . ltrim($reference, '/');
618 if ($this->is_in_repository($reference) && is_readable($file)) {
619 $filename = $storedfile->get_filename();
620 if ($options && isset($options['filename'])) {
621 $filename = $options['filename'];
623 $dontdie = ($options && isset($options['dontdie']));
624 send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie);
625 } else {
626 send_file_not_found();
631 * Is this repository accessing private data?
633 * @return bool
635 public function contains_private_data() {
636 return false;
640 * Return the rootpath of this repository instance.
642 * Trim() is a necessary step to ensure that the subdirectory is not '/'.
644 * @return string path
645 * @throws repository_exception If the subdir is unsafe, or invalid.
647 public function get_rootpath() {
648 global $CFG;
649 $subdir = clean_param(trim($this->subdir, '/'), PARAM_PATH);
650 $path = $CFG->dataroot . '/repository/' . $this->subdir . '/';
651 if ((empty($this->subdir) && !is_numeric($this->subdir)) || $subdir != $this->subdir || !is_dir($path)) {
652 throw new repository_exception('The instance is not properly configured, invalid path.');
654 return $path;
658 * Checks if $path is part of this repository.
660 * Try to prevent $path hacks such as ../ .
662 * We do not use clean_param(, PARAM_PATH) here because it also trims down some
663 * characters that are allowed, like < > ' . But we do ensure that the directory
664 * is safe by checking that it starts with $rootpath.
666 * @param string $path relative path to a file or directory in the repo.
667 * @return boolean false when not.
669 protected function is_in_repository($path) {
670 $rootpath = $this->get_rootpath();
671 if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) {
672 return false;
674 return true;
678 * Returns url of thumbnail file.
680 * @param string $filepath current path in repository (dir and filename)
681 * @param string $thumbsize 'thumb' or 'icon'
682 * @param string $token identifier of the file contents - to prevent browser from caching changed file
683 * @return moodle_url
685 protected function get_thumbnail_url($filepath, $thumbsize, $token) {
686 return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id,
687 '/' . trim($filepath, '/') . '/', $token);
691 * Returns the stored thumbnail file, generates it if not present.
693 * @param string $filepath current path in repository (dir and filename)
694 * @param string $thumbsize 'thumb' or 'icon'
695 * @return null|stored_file
697 public function get_thumbnail($filepath, $thumbsize) {
698 global $CFG;
700 $filepath = trim($filepath, '/');
701 $origfile = $this->get_rootpath() . $filepath;
702 // As thumbnail filename we use original file content hash.
703 if (!$this->is_in_repository($filepath) || !($filecontents = @file_get_contents($origfile))) {
704 // File is not found or is not readable.
705 return null;
707 $filename = file_storage::hash_from_string($filecontents);
709 // Try to get generated thumbnail for this file.
710 $fs = get_file_storage();
711 if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/',
712 $filename))) {
713 // Thumbnail not found . Generate and store thumbnail.
714 require_once($CFG->libdir . '/gdlib.php');
715 if ($thumbsize === 'thumb') {
716 $size = 90;
717 } else {
718 $size = 24;
720 if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) {
721 // Generation failed.
722 return null;
724 $record = array(
725 'contextid' => SYSCONTEXTID,
726 'component' => 'repository_filesystem',
727 'filearea' => $thumbsize,
728 'itemid' => $this->id,
729 'filepath' => '/' . $filepath . '/',
730 'filename' => $filename,
732 $file = $fs->create_file_from_string($record, $data);
734 return $file;
738 * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files.
740 * @param stored_file[] $storedfiles
742 public function remove_obsolete_thumbnails($storedfiles) {
743 // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository).
744 $files = array();
745 foreach ($storedfiles as $file) {
746 if (!isset($files[$file->get_filepath()])) {
747 $files[$file->get_filepath()] = array();
749 $files[$file->get_filepath()][] = $file;
752 // Loop through all files and make sure the original exists and has the same contenthash.
753 $deletedcount = 0;
754 foreach ($files as $filepath => $filesinpath) {
755 if ($filecontents = @file_get_contents($this->get_rootpath() . trim($filepath, '/'))) {
756 // The 'filename' in Moodle file storage is contenthash of the file in filesystem repository.
757 $filename = file_storage::hash_from_string($filecontents);
758 foreach ($filesinpath as $file) {
759 if ($file->get_filename() !== $filename && $file->get_filename() !== '.') {
760 // Contenthash does not match, this is an old thumbnail.
761 $deletedcount++;
762 $file->delete();
765 } else {
766 // Thumbnail exist but file not.
767 foreach ($filesinpath as $file) {
768 if ($file->get_filename() !== '.') {
769 $deletedcount++;
771 $file->delete();
775 if ($deletedcount) {
776 mtrace(" instance {$this->id}: deleted $deletedcount thumbnails");
781 * Gets a file relative to this file in the repository and sends it to the browser.
783 * @param stored_file $mainfile The main file we are trying to access relative files for.
784 * @param string $relativepath the relative path to the file we are trying to access.
786 public function send_relative_file(stored_file $mainfile, $relativepath) {
787 global $CFG;
788 // Check if this repository is allowed to use relative linking.
789 $allowlinks = $this->supports_relative_file();
790 if (!empty($allowlinks)) {
791 // Get path to the mainfile.
792 $mainfilepath = $mainfile->get_source();
794 // Strip out filename from the path.
795 $filename = $mainfile->get_filename();
796 $basepath = strstr($mainfilepath, $filename, true);
798 $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath);
800 // Sanity check to make sure this path is inside this repository and the file exists.
801 if (strpos($fullrelativefilepath, realpath($this->get_rootpath())) === 0 && file_exists($fullrelativefilepath)) {
802 send_file($fullrelativefilepath, basename($relativepath), null, 0);
805 send_file_not_found();
809 * helper function to check if the repository supports send_relative_file.
811 * @return true|false
813 public function supports_relative_file() {
814 return $this->get_option('relativefiles');
819 * Generates and sends the thumbnail for an image in filesystem.
821 * @param stdClass $course course object
822 * @param stdClass $cm course module object
823 * @param stdClass $context context object
824 * @param string $filearea file area
825 * @param array $args extra arguments
826 * @param bool $forcedownload whether or not force download
827 * @param array $options additional options affecting the file serving
828 * @return bool
830 function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
831 global $OUTPUT, $CFG;
832 // Allowed filearea is either thumb or icon - size of the thumbnail.
833 if ($filearea !== 'thumb' && $filearea !== 'icon') {
834 return false;
837 // As itemid we pass repository instance id.
838 $itemid = array_shift($args);
839 // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed).
840 array_pop($args);
841 // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository.
842 $filepath = implode('/', $args);
844 // Make sure file exists in the repository and is accessible.
845 $repo = repository::get_repository_by_id($itemid, $context);
846 $repo->check_capability();
847 // Find stored or generated thumbnail.
848 if (!($file = $repo->get_thumbnail($filepath, $filearea))) {
849 // Generation failed, redirect to default icon for file extension.
850 // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
851 header('Location: ' . $OUTPUT->image_url(file_extension_icon($file, 90)));
853 // The thumbnails should not be changing much, but maybe the default lifetime is too long.
854 $lifetime = $CFG->filelifetime;
855 if ($lifetime > 60*10) {
856 $lifetime = 60*10;
858 send_stored_file($file, $lifetime, 0, $forcedownload, $options);
862 * Cron callback for repository_filesystem. Deletes the thumbnails for deleted or changed files.
864 function repository_filesystem_cron() {
865 $fs = get_file_storage();
866 // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id).
867 $allfiles = array_merge(
868 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'),
869 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon')
871 $filesbyitem = array();
872 foreach ($allfiles as $file) {
873 if (!isset($filesbyitem[$file->get_itemid()])) {
874 $filesbyitem[$file->get_itemid()] = array();
876 $filesbyitem[$file->get_itemid()][] = $file;
878 // Find all instances of repository_filesystem.
879 $instances = repository::get_instances(array('type' => 'filesystem'));
880 // Loop through all itemids of generated thumbnails.
881 foreach ($filesbyitem as $itemid => $files) {
882 if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) {
883 // Instance was deleted.
884 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid);
885 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid);
886 mtrace(" instance $itemid does not exist: deleted all thumbnails");
887 } else {
888 // Instance has some generated thumbnails, check that they are not outdated.
889 $instances[$itemid]->remove_obsolete_thumbnails($files);