Merge branch 'MDL-48494-master-component' of git://github.com/mudrd8mz/moodle
[moodle.git] / repository / filesystem / lib.php
blob1585d883ac596b99235cf45b8d8ef626e220beda
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 $path to browse.
64 * @param string $page page number.
65 * @return array list of files and folders.
67 public function get_listing($path = '', $page = '') {
68 global $OUTPUT;
70 $list = array();
71 $list['list'] = array();
72 $list['manage'] = false;
73 $list['dynload'] = true;
74 $list['nologin'] = true;
75 $list['nosearch'] = true;
76 $list['path'] = array(
77 array('name' => get_string('root', 'repository_filesystem'), 'path' => '')
80 $path = trim($path, '/');
81 if (!$this->is_in_repository($path)) {
82 // In case of doubt on the path, reset to default.
83 $path = '';
85 $abspath = rtrim($this->get_rootpath() . $path, '/') . '/';
87 // Construct the breadcrumb.
88 $trail = '';
89 if ($path !== '') {
90 $parts = explode('/', $path);
91 if (count($parts) > 1) {
92 foreach ($parts as $part) {
93 if (!empty($part)) {
94 $trail .= '/' . $part;
95 $list['path'][] = array('name' => $part, 'path' => $trail);
98 } else {
99 $list['path'][] = array('name' => $path, 'path' => $path);
103 // Retrieve list of files and directories and sort them.
104 $fileslist = array();
105 $dirslist = array();
106 if ($dh = opendir($abspath)) {
107 while (($file = readdir($dh)) != false) {
108 if ($file != '.' and $file != '..') {
109 if (is_file($abspath . $file)) {
110 $fileslist[] = $file;
111 } else {
112 $dirslist[] = $file;
117 core_collator::asort($fileslist, core_collator::SORT_NATURAL);
118 core_collator::asort($dirslist, core_collator::SORT_NATURAL);
120 // Fill the $list['list'].
121 foreach ($dirslist as $file) {
122 $list['list'][] = array(
123 'title' => $file,
124 'children' => array(),
125 'datecreated' => filectime($abspath . $file),
126 'datemodified' => filemtime($abspath . $file),
127 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false),
128 'path' => $path . '/' . $file
131 foreach ($fileslist as $file) {
132 $node = array(
133 'title' => $file,
134 'source' => $path . '/' . $file,
135 'size' => filesize($abspath . $file),
136 'datecreated' => filectime($abspath . $file),
137 'datemodified' => filemtime($abspath . $file),
138 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file, 90))->out(false),
139 'icon' => $OUTPUT->pix_url(file_extension_icon($file, 24))->out(false)
141 if (file_extension_in_typegroup($file, 'image') && ($imageinfo = @getimagesize($abspath . $file))) {
142 // This means it is an image and we can return dimensions and try to generate thumbnail/icon.
143 $token = $node['datemodified'] . $node['size']; // To prevent caching by browser.
144 $node['realthumbnail'] = $this->get_thumbnail_url($path . '/' . $file, 'thumb', $token)->out(false);
145 $node['realicon'] = $this->get_thumbnail_url($path . '/' . $file, 'icon', $token)->out(false);
146 $node['image_width'] = $imageinfo[0];
147 $node['image_height'] = $imageinfo[1];
149 $list['list'][] = $node;
151 $list['list'] = array_filter($list['list'], array($this, 'filter'));
152 return $list;
157 * To check whether the user is logged in.
159 * @return bool
161 public function check_login() {
162 return true;
166 * Show the login screen, if required.
168 * @return string
170 public function print_login() {
171 return true;
175 * Is it possible to do a global search?
177 * @return bool
179 public function global_search() {
180 return false;
184 * Return file path.
185 * @return array
187 public function get_file($file, $title = '') {
188 global $CFG;
189 $file = ltrim($file, '/');
190 if (!$this->is_in_repository($file)) {
191 throw new repository_exception('Invalid file requested.');
193 $file = $this->get_rootpath() . $file;
195 // This is a hack to prevent move_to_file deleting files in local repository.
196 $CFG->repository_no_delete = true;
197 return array('path' => $file, 'url' => '');
201 * Return the source information
203 * @param stdClass $filepath
204 * @return string|null
206 public function get_file_source_info($filepath) {
207 return $filepath;
211 * Logout from repository instance
213 * @return string
215 public function logout() {
216 return true;
220 * Return names of the instance options.
222 * @return array
224 public static function get_instance_option_names() {
225 return array('fs_path', 'relativefiles');
229 * Save settings for repository instance
231 * @param array $options settings
232 * @return bool
234 public function set_option($options = array()) {
235 $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH);
236 $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT);
237 $ret = parent::set_option($options);
238 return $ret;
242 * Edit/Create Instance Settings Moodle form
244 * @param moodleform $mform Moodle form (passed by reference)
246 public static function instance_config_form($mform) {
247 global $CFG;
248 if (has_capability('moodle/site:config', context_system::instance())) {
249 $path = $CFG->dataroot . '/repository/';
250 if (!is_dir($path)) {
251 mkdir($path, $CFG->directorypermissions, true);
253 if ($handle = opendir($path)) {
254 $fieldname = get_string('path', 'repository_filesystem');
255 $choices = array();
256 while (false !== ($file = readdir($handle))) {
257 if (is_dir($path . $file) && $file != '.' && $file != '..') {
258 $choices[$file] = $file;
259 $fieldname = '';
262 if (empty($choices)) {
263 $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path));
264 $mform->addElement('hidden', 'fs_path', '');
265 $mform->setType('fs_path', PARAM_PATH);
266 } else {
267 $mform->addElement('select', 'fs_path', $fieldname, $choices);
268 $mform->addElement('static', null, '', get_string('information', 'repository_filesystem', $path));
270 closedir($handle);
272 $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'),
273 get_string('relativefiles_desc', 'repository_filesystem'));
274 $mform->setType('relativefiles', PARAM_INT);
276 } else {
277 $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin',
278 'repository_filesystem')));
279 return false;
284 * Create an instance for this plug-in
286 * @static
287 * @param string $type the type of the repository
288 * @param int $userid the user id
289 * @param stdClass $context the context
290 * @param array $params the options for this instance
291 * @param int $readonly whether to create it readonly or not (defaults to not)
292 * @return mixed
294 public static function create($type, $userid, $context, $params, $readonly=0) {
295 if (has_capability('moodle/site:config', context_system::instance())) {
296 return parent::create($type, $userid, $context, $params, $readonly);
297 } else {
298 require_capability('moodle/site:config', context_system::instance());
299 return false;
304 * Validate repository plugin instance form
306 * @param moodleform $mform moodle form
307 * @param array $data form data
308 * @param array $errors errors
309 * @return array errors
311 public static function instance_form_validation($mform, $data, $errors) {
312 $fspath = clean_param(trim($data['fs_path'], '/'), PARAM_PATH);
313 if (empty($fspath) && !is_numeric($fspath)) {
314 $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path');
316 return $errors;
320 * User cannot use the external link to dropbox
322 * @return int
324 public function supported_returntypes() {
325 return FILE_INTERNAL | FILE_REFERENCE;
329 * Return human readable reference information
331 * @param string $reference value of DB field files_reference.reference
332 * @param int $filestatus status of the file, 0 - ok, 666 - source missing
333 * @return string
335 public function get_reference_details($reference, $filestatus = 0) {
336 $details = $this->get_name().': '.$reference;
337 if ($filestatus) {
338 return get_string('lostsource', 'repository', $details);
339 } else {
340 return $details;
344 public function sync_reference(stored_file $file) {
345 if ($file->get_referencelastsync() + 60 > time()) {
346 // Does not cost us much to synchronise within our own filesystem, check every 1 minute.
347 return false;
349 static $issyncing = false;
350 if ($issyncing) {
351 // Avoid infinite recursion when calling $file->get_filesize() and get_contenthash().
352 return false;
354 $filepath = $this->get_rootpath() . ltrim($file->get_reference(), '/');
355 if ($this->is_in_repository($file->get_reference()) && file_exists($filepath) && is_readable($filepath)) {
356 $fs = get_file_storage();
357 $issyncing = true;
358 if (file_extension_in_typegroup($filepath, 'web_image')) {
359 $contenthash = sha1_file($filepath);
360 if ($file->get_contenthash() == $contenthash) {
361 // File did not change since the last synchronisation.
362 $filesize = filesize($filepath);
363 } else {
364 // Copy file into moodle filepool (used to generate an image thumbnail).
365 list($contenthash, $filesize, $newfile) = $fs->add_file_to_pool($filepath);
367 } else {
368 // Update only file size so file will NOT be copied into moodle filepool.
369 $emptyfile = $contenthash = sha1('');
370 $currentcontenthash = $file->get_contenthash();
371 if ($currentcontenthash !== $emptyfile && $currentcontenthash === sha1_file($filepath)) {
372 // File content was synchronised and has not changed since then, leave it.
373 $contenthash = null;
375 $filesize = filesize($filepath);
377 $issyncing = false;
378 $file->set_synchronized($contenthash, $filesize);
379 } else {
380 $file->set_missingsource();
382 return true;
386 * Repository method to serve the referenced file
388 * @see send_stored_file
390 * @param stored_file $storedfile the file that contains the reference
391 * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
392 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
393 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
394 * @param array $options additional options affecting the file serving
396 public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
397 $reference = $storedfile->get_reference();
398 $file = $this->get_rootpath() . ltrim($reference, '/');
399 if ($this->is_in_repository($reference) && is_readable($file)) {
400 $filename = $storedfile->get_filename();
401 if ($options && isset($options['filename'])) {
402 $filename = $options['filename'];
404 $dontdie = ($options && isset($options['dontdie']));
405 send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie);
406 } else {
407 send_file_not_found();
412 * Is this repository accessing private data?
414 * @return bool
416 public function contains_private_data() {
417 return false;
421 * Return the rootpath of this repository instance.
423 * Trim() is a necessary step to ensure that the subdirectory is not '/'.
425 * @return string path
426 * @throws repository_exception If the subdir is unsafe, or invalid.
428 public function get_rootpath() {
429 global $CFG;
430 $subdir = clean_param(trim($this->subdir, '/'), PARAM_PATH);
431 $path = $CFG->dataroot . '/repository/' . $this->subdir . '/';
432 if ((empty($this->subdir) && !is_numeric($this->subdir)) || $subdir != $this->subdir || !is_dir($path)) {
433 throw new repository_exception('The instance is not properly configured, invalid path.');
435 return $path;
439 * Checks if $path is part of this repository.
441 * Try to prevent $path hacks such as ../ .
443 * We do not use clean_param(, PARAM_PATH) here because it also trims down some
444 * characters that are allowed, like < > ' . But we do ensure that the directory
445 * is safe by checking that it starts with $rootpath.
447 * @param string $path relative path to a file or directory in the repo.
448 * @return boolean false when not.
450 protected function is_in_repository($path) {
451 $rootpath = $this->get_rootpath();
452 if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) {
453 return false;
455 return true;
459 * Returns url of thumbnail file.
461 * @param string $filepath current path in repository (dir and filename)
462 * @param string $thumbsize 'thumb' or 'icon'
463 * @param string $token identifier of the file contents - to prevent browser from caching changed file
464 * @return moodle_url
466 protected function get_thumbnail_url($filepath, $thumbsize, $token) {
467 return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id,
468 '/' . trim($filepath, '/') . '/', $token);
472 * Returns the stored thumbnail file, generates it if not present.
474 * @param string $filepath current path in repository (dir and filename)
475 * @param string $thumbsize 'thumb' or 'icon'
476 * @return null|stored_file
478 public function get_thumbnail($filepath, $thumbsize) {
479 global $CFG;
481 $filepath = trim($filepath, '/');
482 $origfile = $this->get_rootpath() . $filepath;
483 // As thumbnail filename we use original file content hash.
484 if (!$this->is_in_repository($filepath) || !($filecontents = @file_get_contents($origfile))) {
485 // File is not found or is not readable.
486 return null;
488 $filename = sha1($filecontents);
490 // Try to get generated thumbnail for this file.
491 $fs = get_file_storage();
492 if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/',
493 $filename))) {
494 // Thumbnail not found . Generate and store thumbnail.
495 require_once($CFG->libdir . '/gdlib.php');
496 if ($thumbsize === 'thumb') {
497 $size = 90;
498 } else {
499 $size = 24;
501 if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) {
502 // Generation failed.
503 return null;
505 $record = array(
506 'contextid' => SYSCONTEXTID,
507 'component' => 'repository_filesystem',
508 'filearea' => $thumbsize,
509 'itemid' => $this->id,
510 'filepath' => '/' . $filepath . '/',
511 'filename' => $filename,
513 $file = $fs->create_file_from_string($record, $data);
515 return $file;
519 * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files.
521 * @param stored_file[] $storedfiles
523 public function remove_obsolete_thumbnails($storedfiles) {
524 // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository).
525 $files = array();
526 foreach ($storedfiles as $file) {
527 if (!isset($files[$file->get_filepath()])) {
528 $files[$file->get_filepath()] = array();
530 $files[$file->get_filepath()][] = $file;
533 // Loop through all files and make sure the original exists and has the same contenthash.
534 $deletedcount = 0;
535 foreach ($files as $filepath => $filesinpath) {
536 if ($filecontents = @file_get_contents($this->get_rootpath() . trim($filepath, '/'))) {
537 // The 'filename' in Moodle file storage is contenthash of the file in filesystem repository.
538 $filename = sha1($filecontents);
539 foreach ($filesinpath as $file) {
540 if ($file->get_filename() !== $filename && $file->get_filename() !== '.') {
541 // Contenthash does not match, this is an old thumbnail.
542 $deletedcount++;
543 $file->delete();
546 } else {
547 // Thumbnail exist but file not.
548 foreach ($filesinpath as $file) {
549 if ($file->get_filename() !== '.') {
550 $deletedcount++;
552 $file->delete();
556 if ($deletedcount) {
557 mtrace(" instance {$this->id}: deleted $deletedcount thumbnails");
562 * Gets a file relative to this file in the repository and sends it to the browser.
564 * @param stored_file $mainfile The main file we are trying to access relative files for.
565 * @param string $relativepath the relative path to the file we are trying to access.
567 public function send_relative_file(stored_file $mainfile, $relativepath) {
568 global $CFG;
569 // Check if this repository is allowed to use relative linking.
570 $allowlinks = $this->supports_relative_file();
571 if (!empty($allowlinks)) {
572 // Get path to the mainfile.
573 $mainfilepath = $mainfile->get_source();
575 // Strip out filename from the path.
576 $filename = $mainfile->get_filename();
577 $basepath = strstr($mainfilepath, $filename, true);
579 $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath);
581 // Sanity check to make sure this path is inside this repository and the file exists.
582 if (strpos($fullrelativefilepath, realpath($this->get_rootpath())) === 0 && file_exists($fullrelativefilepath)) {
583 send_file($fullrelativefilepath, basename($relativepath), null, 0);
586 send_file_not_found();
590 * helper function to check if the repository supports send_relative_file.
592 * @return true|false
594 public function supports_relative_file() {
595 return $this->get_option('relativefiles');
600 * Generates and sends the thumbnail for an image in filesystem.
602 * @param stdClass $course course object
603 * @param stdClass $cm course module object
604 * @param stdClass $context context object
605 * @param string $filearea file area
606 * @param array $args extra arguments
607 * @param bool $forcedownload whether or not force download
608 * @param array $options additional options affecting the file serving
609 * @return bool
611 function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
612 global $OUTPUT, $CFG;
613 // Allowed filearea is either thumb or icon - size of the thumbnail.
614 if ($filearea !== 'thumb' && $filearea !== 'icon') {
615 return false;
618 // As itemid we pass repository instance id.
619 $itemid = array_shift($args);
620 // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed).
621 array_pop($args);
622 // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository.
623 $filepath = implode('/', $args);
625 // Make sure file exists in the repository and is accessible.
626 $repo = repository::get_repository_by_id($itemid, $context);
627 $repo->check_capability();
628 // Find stored or generated thumbnail.
629 if (!($file = $repo->get_thumbnail($filepath, $filearea))) {
630 // Generation failed, redirect to default icon for file extension.
631 redirect($OUTPUT->pix_url(file_extension_icon($file, 90)));
633 // The thumbnails should not be changing much, but maybe the default lifetime is too long.
634 $lifetime = $CFG->filelifetime;
635 if ($lifetime > 60*10) {
636 $lifetime = 60*10;
638 send_stored_file($file, $lifetime, 0, $forcedownload, $options);
642 * Cron callback for repository_filesystem. Deletes the thumbnails for deleted or changed files.
644 function repository_filesystem_cron() {
645 $fs = get_file_storage();
646 // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id).
647 $allfiles = array_merge(
648 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'),
649 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon')
651 $filesbyitem = array();
652 foreach ($allfiles as $file) {
653 if (!isset($filesbyitem[$file->get_itemid()])) {
654 $filesbyitem[$file->get_itemid()] = array();
656 $filesbyitem[$file->get_itemid()][] = $file;
658 // Find all instances of repository_filesystem.
659 $instances = repository::get_instances(array('type' => 'filesystem'));
660 // Loop through all itemids of generated thumbnails.
661 foreach ($filesbyitem as $itemid => $files) {
662 if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) {
663 // Instance was deleted.
664 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid);
665 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid);
666 mtrace(" instance $itemid does not exist: deleted all thumbnails");
667 } else {
668 // Instance has some generated thumbnails, check that they are not outdated.
669 $instances[$itemid]->remove_obsolete_thumbnails($files);