MDL-41580 SCORM: allow imsmanifest.xml to be used in file system repos
[moodle.git] / repository / filesystem / lib.php
CommitLineData
4317f92f 1<?php
10d53fd3
DC
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
67233725
DC
17/**
18 * This plugin is used to access files on server file system
19 *
20 * @since 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 */
25require_once($CFG->dirroot . '/repository/lib.php');
26require_once($CFG->libdir . '/filelib.php');
27
fdcf5320 28/**
29 * repository_filesystem class
67233725 30 *
fdcf5320 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 *
d078f6d3 35 * @package repository
67233725 36 * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org}
d078f6d3 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
fdcf5320 38 */
520de343 39class repository_filesystem extends repository {
67233725
DC
40
41 /**
42 * Constructor
43 *
44 * @param int $repositoryid repository ID
45 * @param int $context context ID
46 * @param array $options
47 */
447c7a19 48 public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
fdcf5320 49 global $CFG;
520de343 50 parent::__construct($repositoryid, $context, $options);
c122f484 51 $root = $CFG->dataroot . '/repository/';
873f2e0f 52 $subdir = $this->get_option('fs_path');
c122f484
FM
53
54 $this->root_path = $root;
55 if (!empty($subdir)) {
56 $this->root_path .= $subdir . '/';
57 }
58
e437618b 59 if (!empty($options['ajax'])) {
fdcf5320 60 if (!is_dir($this->root_path)) {
8512eebe 61 $created = mkdir($this->root_path, $CFG->directorypermissions, true);
520de343 62 $ret = array();
63 $ret['msg'] = get_string('invalidpath', 'repository_filesystem');
64 $ret['nosearch'] = true;
93e9aa27 65 if ($options['ajax'] && !$created) {
fdcf5320 66 echo json_encode($ret);
67 exit;
520de343 68 }
69 }
520de343 70 }
71 }
72 public function get_listing($path = '', $page = '') {
390baf46 73 global $CFG, $OUTPUT;
520de343 74 $list = array();
75 $list['list'] = array();
76 // process breacrumb trail
77 $list['path'] = array(
5bdf63cc 78 array('name'=>get_string('root', 'repository_filesystem'), 'path'=>'')
520de343 79 );
80 $trail = '';
81 if (!empty($path)) {
82 $parts = explode('/', $path);
83 if (count($parts) > 1) {
84 foreach ($parts as $part) {
4a9aff79 85 if (!empty($part)) {
86 $trail .= ('/'.$part);
87 $list['path'][] = array('name'=>$part, 'path'=>$trail);
88 }
520de343 89 }
90 } else {
91 $list['path'][] = array('name'=>$path, 'path'=>$path);
92 }
93 $this->root_path .= ($path.'/');
94 }
520de343 95 $list['manage'] = false;
520de343 96 $list['dynload'] = true;
520de343 97 $list['nologin'] = true;
520de343 98 $list['nosearch'] = true;
7355640a
MG
99 // retrieve list of files and directories and sort them
100 $fileslist = array();
101 $dirslist = array();
520de343 102 if ($dh = opendir($this->root_path)) {
103 while (($file = readdir($dh)) != false) {
104 if ( $file != '.' and $file !='..') {
7355640a
MG
105 if (is_file($this->root_path.$file)) {
106 $fileslist[] = $file;
520de343 107 } else {
7355640a 108 $dirslist[] = $file;
520de343 109 }
110 }
111 }
112 }
2f1e464a
PS
113 core_collator::asort($fileslist, core_collator::SORT_STRING);
114 core_collator::asort($dirslist, core_collator::SORT_STRING);
7355640a
MG
115 // fill the $list['list']
116 foreach ($dirslist as $file) {
117 if (!empty($path)) {
118 $current_path = $path . '/'. $file;
119 } else {
120 $current_path = $file;
121 }
122 $list['list'][] = array(
123 'title' => $file,
124 'children' => array(),
125 'datecreated' => filectime($this->root_path.$file),
126 'datemodified' => filemtime($this->root_path.$file),
127 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false),
128 'path' => $current_path
129 );
130 }
131 foreach ($fileslist as $file) {
2ef39ca4 132 $node = array(
7355640a
MG
133 'title' => $file,
134 'source' => $path.'/'.$file,
135 'size' => filesize($this->root_path.$file),
136 'datecreated' => filectime($this->root_path.$file),
137 'datemodified' => filemtime($this->root_path.$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)
140 );
a7e505e6 141 if (file_extension_in_typegroup($file, 'image') && ($imageinfo = @getimagesize($this->root_path . $file))) {
2ef39ca4 142 // This means it is an image and we can return dimensions and try to generate thumbnail/icon.
a7e505e6
MG
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);
2ef39ca4
MG
146 $node['image_width'] = $imageinfo[0];
147 $node['image_height'] = $imageinfo[1];
148 }
149 $list['list'][] = $node;
7355640a 150 }
fb4ee704 151 $list['list'] = array_filter($list['list'], array($this, 'filter'));
520de343 152 return $list;
153 }
2ef39ca4 154
520de343 155 public function check_login() {
156 return true;
157 }
520de343 158 public function print_login() {
159 return true;
160 }
520de343 161 public function global_search() {
162 return false;
163 }
67233725 164
951d2d55
DC
165 /**
166 * Return file path
167 * @return array
168 */
520de343 169 public function get_file($file, $title = '') {
170 global $CFG;
171 if ($file{0} == '/') {
172 $file = $this->root_path.substr($file, 1, strlen($file)-1);
951d2d55
DC
173 } else {
174 $file = $this->root_path.$file;
520de343 175 }
176 // this is a hack to prevent move_to_file deleteing files
177 // in local repository
178 $CFG->repository_no_delete = true;
85b5ed5d 179 return array('path'=>$file, 'url'=>'');
520de343 180 }
181
d6453211
DC
182 /**
183 * Return the source information
184 *
185 * @param stdClass $filepath
186 * @return string|null
187 */
188 public function get_file_source_info($filepath) {
189 return $filepath;
190 }
191
520de343 192 public function logout() {
193 return true;
194 }
195
196 public static function get_instance_option_names() {
361a47d4 197 return array('fs_path', 'relativefiles');
520de343 198 }
199
b2f8adf4 200 public function set_option($options = array()) {
201 $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH);
361a47d4 202 $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT);
b2f8adf4 203 $ret = parent::set_option($options);
204 return $ret;
205 }
8e5af6cf 206
4a126f17 207 public static function instance_config_form($mform) {
49d20def 208 global $CFG, $PAGE;
0601e0ee 209 if (has_capability('moodle/site:config', context_system::instance())) {
49d20def
DC
210 $path = $CFG->dataroot . '/repository/';
211 if (!is_dir($path)) {
8512eebe 212 mkdir($path, $CFG->directorypermissions, true);
93e9aa27 213 }
49d20def
DC
214 if ($handle = opendir($path)) {
215 $fieldname = get_string('path', 'repository_filesystem');
216 $choices = array();
217 while (false !== ($file = readdir($handle))) {
218 if (is_dir($path.$file) && $file != '.' && $file!= '..') {
219 $choices[$file] = $file;
220 $fieldname = '';
221 }
222 }
223 if (empty($choices)) {
224 $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path));
6b172cdc 225 $mform->addElement('hidden', 'fs_path', '');
8fb59b10 226 $mform->setType('fs_path', PARAM_PATH);
49d20def
DC
227 } else {
228 $mform->addElement('select', 'fs_path', $fieldname, $choices);
229 $mform->addElement('static', null, '', get_string('information','repository_filesystem', $path));
230 }
231 closedir($handle);
873f2e0f 232 }
361a47d4
DM
233 $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'),
234 get_string('relativefiles_desc', 'repository_filesystem'));
235 $mform->setType('relativefiles', PARAM_INT);
236
49d20def
DC
237 } else {
238 $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin', 'repository_filesystem')));
239 return false;
93e9aa27 240 }
93e9aa27 241 }
8e5af6cf 242
49d20def
DC
243 public static function create($type, $userid, $context, $params, $readonly=0) {
244 global $PAGE;
0601e0ee 245 if (has_capability('moodle/site:config', context_system::instance())) {
49d20def
DC
246 return parent::create($type, $userid, $context, $params, $readonly);
247 } else {
0601e0ee 248 require_capability('moodle/site:config', context_system::instance());
49d20def
DC
249 return false;
250 }
251 }
6b172cdc
DC
252 public static function instance_form_validation($mform, $data, $errors) {
253 if (empty($data['fs_path'])) {
254 $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path');
255 }
256 return $errors;
257 }
67233725
DC
258
259 /**
260 * User cannot use the external link to dropbox
261 *
262 * @return int
263 */
264 public function supported_returntypes() {
265 return FILE_INTERNAL | FILE_REFERENCE;
266 }
267
96221c60
MG
268 /**
269 * Return reference file life time
270 *
271 * @param string $ref
272 * @return int
273 */
274 public function get_reference_file_lifetime($ref) {
275 // Does not cost us much to synchronise within our own filesystem, set to 1 minute
276 return 60;
277 }
278
279 /**
280 * Return human readable reference information
281 *
282 * @param string $reference value of DB field files_reference.reference
283 * @param int $filestatus status of the file, 0 - ok, 666 - source missing
284 * @return string
285 */
286 public function get_reference_details($reference, $filestatus = 0) {
287 $details = $this->get_name().': '.$reference;
288 if ($filestatus) {
289 return get_string('lostsource', 'repository', $details);
290 } else {
291 return $details;
292 }
293 }
294
67233725 295 /**
0b2bfbd1 296 * Returns information about file in this repository by reference
67233725 297 *
0b2bfbd1
MG
298 * Returns null if file not found or is not readable
299 *
67233725 300 * @param stdClass $reference file reference db record
0b2bfbd1 301 * @return stdClass|null contains one of the following:
63d8ccef
MG
302 * - 'filesize' if file should not be copied to moodle filepool
303 * - 'filepath' if file should be copied to moodle filepool
67233725
DC
304 */
305 public function get_file_by_reference($reference) {
306 $ref = $reference->reference;
307 if ($ref{0} == '/') {
308 $filepath = $this->root_path.substr($ref, 1, strlen($ref)-1);
309 } else {
310 $filepath = $this->root_path.$ref;
311 }
0b2bfbd1 312 if (file_exists($filepath) && is_readable($filepath)) {
63d8ccef
MG
313 if (file_extension_in_typegroup($filepath, 'web_image')) {
314 // return path to image files so it will be copied into moodle filepool
315 // we need the file in filepool to generate an image thumbnail
316 return (object)array('filepath' => $filepath);
317 } else {
318 // return just the file size so file will NOT be copied into moodle filepool
319 return (object)array(
320 'filesize' => filesize($filepath)
321 );
322 }
0b2bfbd1
MG
323 } else {
324 return null;
325 }
67233725
DC
326 }
327
328 /**
0b2bfbd1
MG
329 * Repository method to serve the referenced file
330 *
331 * @see send_stored_file
67233725 332 *
0b2bfbd1 333 * @param stored_file $storedfile the file that contains the reference
67233725
DC
334 * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours)
335 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
336 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
337 * @param array $options additional options affecting the file serving
338 */
339 public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) {
340 $reference = $storedfile->get_reference();
341 if ($reference{0} == '/') {
342 $file = $this->root_path.substr($reference, 1, strlen($reference)-1);
343 } else {
344 $file = $this->root_path.$reference;
345 }
0b2bfbd1
MG
346 if (is_readable($file)) {
347 $filename = $storedfile->get_filename();
348 if ($options && isset($options['filename'])) {
349 $filename = $options['filename'];
350 }
351 $dontdie = ($options && isset($options['dontdie']));
352 send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie);
353 } else {
354 send_file_not_found();
355 }
67233725 356 }
31581ae6
FM
357
358 /**
359 * Is this repository accessing private data?
360 *
361 * @return bool
362 */
363 public function contains_private_data() {
364 return false;
365 }
2ef39ca4
MG
366
367 /**
368 * Returns url of thumbnail file.
369 *
370 * @param string $filepath current path in repository (dir and filename)
371 * @param string $thumbsize 'thumb' or 'icon'
372 * @param string $token identifier of the file contents - to prevent browser from caching changed file
373 * @return moodle_url
374 */
375 protected function get_thumbnail_url($filepath, $thumbsize, $token) {
376 return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id,
a7e505e6 377 '/' . trim($filepath, '/') . '/', $token);
2ef39ca4
MG
378 }
379
380 /**
381 * Returns the stored thumbnail file, generates it if not present.
382 *
383 * @param string $filepath current path in repository (dir and filename)
384 * @param string $thumbsize 'thumb' or 'icon'
385 * @return null|stored_file
386 */
387 public function get_thumbnail($filepath, $thumbsize) {
388 global $CFG;
389
390 $filepath = trim($filepath, '/');
a7e505e6 391 $origfile = $this->root_path . $filepath;
2ef39ca4
MG
392 // As thumbnail filename we use original file content hash.
393 if (!($filecontents = @file_get_contents($origfile))) {
394 // File is not found or is not readable.
395 return null;
396 }
397 $filename = sha1($filecontents);
398 unset($filecontents);
399
400 // Try to get generated thumbnail for this file.
401 $fs = get_file_storage();
a7e505e6
MG
402 if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/', $filename))) {
403 // Thumbnail not found . Generate and store thumbnail.
404 require_once($CFG->libdir . '/gdlib.php');
2ef39ca4
MG
405 if ($thumbsize === 'thumb') {
406 $size = 90;
407 } else {
408 $size = 24;
409 }
410 if (!$data = @generate_image_thumbnail($origfile, $size, $size)) {
411 // Generation failed.
412 return null;
413 }
414 $record = array(
415 'contextid' => SYSCONTEXTID,
416 'component' => 'repository_filesystem',
417 'filearea' => $thumbsize,
418 'itemid' => $this->id,
a7e505e6 419 'filepath' => '/' . $filepath . '/',
2ef39ca4
MG
420 'filename' => $filename,
421 );
422 $file = $fs->create_file_from_string($record, $data);
423 }
424 return $file;
425 }
426
427 /**
a7e505e6
MG
428 * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files.
429 *
430 * @param stored_file[] $storedfiles
2ef39ca4 431 */
a7e505e6 432 public function remove_obsolete_thumbnails($storedfiles) {
2ef39ca4
MG
433 // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository).
434 $files = array();
a7e505e6
MG
435 foreach ($storedfiles as $file) {
436 if (!isset($files[$file->get_filepath()])) {
437 $files[$file->get_filepath()] = array();
2ef39ca4 438 }
a7e505e6 439 $files[$file->get_filepath()][] = $file;
2ef39ca4 440 }
2ef39ca4
MG
441
442 // Loop through all files and make sure the original exists and has the same contenthash.
443 $deletedcount = 0;
a7e505e6
MG
444 foreach ($files as $filepath => $filesinpath) {
445 if ($filecontents = @file_get_contents($this->root_path . trim($filepath, '/'))) {
2ef39ca4
MG
446 // 'filename' in Moodle file storage is contenthash of the file in filesystem repository.
447 $filename = sha1($filecontents);
a7e505e6
MG
448 foreach ($filesinpath as $file) {
449 if ($file->get_filename() !== $filename && $file->get_filename() !== '.') {
2ef39ca4
MG
450 // Contenthash does not match, this is an old thumbnail.
451 $deletedcount++;
a7e505e6 452 $file->delete();
2ef39ca4
MG
453 }
454 }
455 } else {
456 // Thumbnail exist but file not.
a7e505e6
MG
457 foreach ($filesinpath as $file) {
458 if ($file->get_filename() !== '.') {
2ef39ca4
MG
459 $deletedcount++;
460 }
a7e505e6 461 $file->delete();
2ef39ca4
MG
462 }
463 }
464 }
465 if ($deletedcount) {
466 mtrace(" instance {$this->id}: deleted $deletedcount thumbnails");
467 }
468 }
361a47d4
DM
469
470 /**
471 * Gets a file relative to this file in the repository and sends it to the browser.
472 *
473 * @param stored_file $mainfile The main file we are trying to access relative files for.
474 * @param string $relativepath the relative path to the file we are trying to access.
475 */
476 public function send_relative_file(stored_file $mainfile, $relativepath) {
477 global $CFG;
478 // Check if this repository is allowed to use relative linking.
479 $allowlinks = $this->supports_relative_file();
480 $lifetime = isset($CFG->filelifetime) ? $CFG->filelifetime : 86400;
481 if (!empty($allowlinks)) {
482 // Get path to the mainfile.
483 $mainfilepath = $mainfile->get_source();
484
485 // Strip out filename from the path.
486 $filename = $mainfile->get_filename();
487 $basepath = strstr($mainfilepath, $filename, true);
488
489 $fullrelativefilepath = realpath($this->root_path.$basepath.$relativepath);
490
491 // Sanity check to make sure this path is inside this repository and the file exists.
492 if (strpos($fullrelativefilepath, $this->root_path) === 0 && file_exists($fullrelativefilepath)) {
493 send_file($fullrelativefilepath, basename($relativepath), $lifetime, 0);
494 }
495 }
496 send_file_not_found();
497 }
498
499 /**
500 * helper function to check if the repository supports send_relative_file.
501 *
502 * @return true|false
503 */
504 public function supports_relative_file() {
505 return $this->get_option('relativefiles');
506 }
2ef39ca4
MG
507}
508
509/**
510 * Generates and sends the thumbnail for an image in filesystem.
511 *
512 * @param stdClass $course course object
513 * @param stdClass $cm course module object
514 * @param stdClass $context context object
515 * @param string $filearea file area
516 * @param array $args extra arguments
517 * @param bool $forcedownload whether or not force download
518 * @param array $options additional options affecting the file serving
519 * @return bool
520 */
521function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
522 global $OUTPUT;
523 // Allowed filearea is either thumb or icon - size of the thumbnail.
524 if ($filearea !== 'thumb' && $filearea !== 'icon') {
525 return false;
526 }
527
528 // As itemid we pass repository instance id.
529 $itemid = array_shift($args);
530 // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed).
531 array_pop($args);
532 // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository.
533 $filepath = implode('/', $args);
534
535 // Make sure file exists in the repository and is accessible.
536 $repo = repository::get_repository_by_id($itemid, $context);
537 $repo->check_capability();
538 // Find stored or generated thumbnail.
539 if (!($file = $repo->get_thumbnail($filepath, $filearea))) {
540 // Generation failed, redirect to default icon for file extension.
541 redirect($OUTPUT->pix_url(file_extension_icon($file, 90)));
542 }
543 send_stored_file($file, 360, 0, $forcedownload, $options);
520de343 544}
2ef39ca4
MG
545
546/**
547 * Cron callback for repository_filesystem. Deletes the thumbnails for deleted or changed files.
548 */
549function repository_filesystem_cron() {
a7e505e6
MG
550 $fs = get_file_storage();
551 // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id).
552 $allfiles = array_merge(
553 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'),
554 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon')
555 );
556 $filesbyitem = array();
557 foreach ($allfiles as $file) {
558 if (!isset($filesbyitem[$file->get_itemid()])) {
559 $filesbyitem[$file->get_itemid()] = array();
560 }
561 $filesbyitem[$file->get_itemid()][] = $file;
562 }
563 // Find all instances of repository_filesystem.
2ef39ca4 564 $instances = repository::get_instances(array('type' => 'filesystem'));
a7e505e6
MG
565 // Loop through all itemids of generated thumbnails.
566 foreach ($filesbyitem as $itemid => $files) {
567 if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) {
2ef39ca4 568 // Instance was deleted.
2ef39ca4
MG
569 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid);
570 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid);
571 mtrace(" instance $itemid does not exist: deleted all thumbnails");
572 } else {
573 // Instance has some generated thumbnails, check that they are not outdated.
a7e505e6 574 $instances[$itemid]->remove_obsolete_thumbnails($files);
2ef39ca4
MG
575 }
576 }
577}