See Release Notes
Long Term Support Release
Differences Between: [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * 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'); 27 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 { 40 41 /** 42 * The subdirectory of the instance. 43 * 44 * @var string 45 */ 46 protected $subdir; 47 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 } 59 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; 69 70 $list = array( 71 'list' => array(), 72 'manage' => false, 73 'dynload' => true, 74 'nologin' => true, 75 'path' => array() 76 ); 77 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); 83 84 // Is that a search? 85 if ($mode === 'search') { 86 return $this->search($path, $page); 87 } 88 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, '/') . '/'; 97 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); 114 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 } 122 123 $list['path'] = $this->build_breadcrumb($fullpath); 124 $list['list'] = array_filter($list['list'], array($this, 'filter')); 125 126 return $list; 127 } 128 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; 142 143 $query = core_text::strtolower($query); 144 $remainingdirs = 1000; 145 $remainingobjects = 5000; 146 $perpage = 50; 147 148 // Because the repository API is weird, the first page is 0, but it should be 1. 149 if (!$page) { 150 $page = 1; 151 } 152 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 } 157 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); 170 171 // Pre-search initialisation. 172 $rootpath = $this->get_rootpath(); 173 $found = 0; 174 $toexplore = array(''); 175 176 // Retrieve list of matching files and directories. 177 $matches = array(); 178 while (($path = array_shift($toexplore)) !== null) { 179 $remainingdirs--; 180 181 if ($objects = scandir($rootpath . $path)) { 182 foreach ($objects as $object) { 183 $objectabspath = $rootpath . $path . $object; 184 if ($object == '.' || $object == '..') { 185 continue; 186 } 187 188 $remainingobjects--; 189 $isdir = is_dir($objectabspath); 190 191 // It is a match! 192 if (strpos(core_text::strtolower($object), $query) !== false) { 193 $found++; 194 $matches[] = array($path, $object, $isdir); 195 196 // That's enough, no need to find more. 197 if ($found >= $limit) { 198 break 2; 199 } 200 } 201 202 // I've seen enough files, I give up! 203 if ($remainingobjects <= 0) { 204 break 2; 205 } 206 207 // Add the directory to things to explore later. 208 if ($isdir) { 209 $toexplore[] = $path . trim($object, '/') . '/'; 210 } 211 } 212 } 213 214 if ($remainingdirs <= 0) { 215 break; 216 } 217 } 218 219 // Extract the results from all the matches. 220 $matches = array_slice($matches, $from, $perpage); 221 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; 226 227 // We reached the end of the repository, or our limits. 228 } else { 229 $SESSION->repository_filesystem_search['from'] = 0; 230 $pages = 0; 231 } 232 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 } 239 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); 247 248 return $list; 249 } 250 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 )); 262 263 $crumbs = explode('|', $path); 264 $trail = ''; 265 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; 275 276 case 'browse': 277 $breadcrumb[] = array( 278 'name' => $display, 279 'path' => $this->build_node_path($mode, $nodepath, $display, $trail), 280 ); 281 break; 282 } 283 284 $lastcrumb = end($breadcrumb); 285 $trail = $lastcrumb['path']; 286 } 287 288 return $breadcrumb; 289 } 290 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; 303 304 $relpath = trim($path, '/') . '/' . $name; 305 $abspath = $rootpath . $relpath; 306 $node = array( 307 'title' => $name, 308 'datecreated' => filectime($abspath), 309 'datemodified' => filemtime($abspath), 310 ); 311 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); 316 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; 323 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 } 333 334 return $node; 335 } 336 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 } 353 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 } 370 371 /** 372 * To check whether the user is logged in. 373 * 374 * @return bool 375 */ 376 public function check_login() { 377 return true; 378 } 379 380 /** 381 * Show the login screen, if required. 382 * 383 * @return string 384 */ 385 public function print_login() { 386 return true; 387 } 388 389 /** 390 * Is it possible to do a global search? 391 * 392 * @return bool 393 */ 394 public function global_search() { 395 return false; 396 } 397 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; 409 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 } 414 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 } 424 425 /** 426 * Logout from repository instance 427 * 428 * @return string 429 */ 430 public function logout() { 431 return true; 432 } 433 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 } 442 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 } 455 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); 490 491 } else { 492 $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin', 493 'repository_filesystem'))); 494 return false; 495 } 496 } 497 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 } 517 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 } 533 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 } 542 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 } 558 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 = 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; 583 } 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; 592 } 593 $filesize = filesize($filepath); 594 } 595 $issyncing = false; 596 $modified = filemtime($filepath); 597 $file->set_synchronized($contenthash, $filesize, 0, $modified); 598 } else { 599 $file->set_missingsource(); 600 } 601 return true; 602 } 603 604 /** 605 * Repository method to serve the referenced file 606 * 607 * @see send_stored_file 608 * 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 614 */ 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']; 622 } 623 $dontdie = ($options && isset($options['dontdie'])); 624 send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie); 625 } else { 626 send_file_not_found(); 627 } 628 } 629 630 /** 631 * Is this repository accessing private data? 632 * 633 * @return bool 634 */ 635 public function contains_private_data() { 636 return false; 637 } 638 639 /** 640 * Return the rootpath of this repository instance. 641 * 642 * Trim() is a necessary step to ensure that the subdirectory is not '/'. 643 * 644 * @return string path 645 * @throws repository_exception If the subdir is unsafe, or invalid. 646 */ 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.'); 653 } 654 return $path; 655 } 656 657 /** 658 * Checks if $path is part of this repository. 659 * 660 * Try to prevent $path hacks such as ../ . 661 * 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. 665 * 666 * @param string $path relative path to a file or directory in the repo. 667 * @return boolean false when not. 668 */ 669 protected function is_in_repository($path) { 670 $rootpath = $this->get_rootpath(); 671 if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) { 672 return false; 673 } 674 return true; 675 } 676 677 /** 678 * Returns url of thumbnail file. 679 * 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 684 */ 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); 688 } 689 690 /** 691 * Returns the stored thumbnail file, generates it if not present. 692 * 693 * @param string $filepath current path in repository (dir and filename) 694 * @param string $thumbsize 'thumb' or 'icon' 695 * @return null|stored_file 696 */ 697 public function get_thumbnail($filepath, $thumbsize) { 698 global $CFG; 699 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; 706 } 707 $filename = file_storage::hash_from_string($filecontents); 708 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; 719 } 720 if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) { 721 // Generation failed. 722 return null; 723 } 724 $record = array( 725 'contextid' => SYSCONTEXTID, 726 'component' => 'repository_filesystem', 727 'filearea' => $thumbsize, 728 'itemid' => $this->id, 729 'filepath' => '/' . $filepath . '/', 730 'filename' => $filename, 731 ); 732 $file = $fs->create_file_from_string($record, $data); 733 } 734 return $file; 735 } 736 737 /** 738 * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files. 739 * 740 * @param stored_file[] $storedfiles 741 */ 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(); 748 } 749 $files[$file->get_filepath()][] = $file; 750 } 751 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(); 763 } 764 } 765 } else { 766 // Thumbnail exist but file not. 767 foreach ($filesinpath as $file) { 768 if ($file->get_filename() !== '.') { 769 $deletedcount++; 770 } 771 $file->delete(); 772 } 773 } 774 } 775 if ($deletedcount) { 776 mtrace(" instance {$this->id}: deleted $deletedcount thumbnails"); 777 } 778 } 779 780 /** 781 * Gets a file relative to this file in the repository and sends it to the browser. 782 * 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. 785 */ 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(); 793 794 // Strip out filename from the path. 795 $filename = $mainfile->get_filename(); 796 $basepath = strstr($mainfilepath, $filename, true); 797 798 $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath); 799 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); 803 } 804 } 805 send_file_not_found(); 806 } 807 808 /** 809 * helper function to check if the repository supports send_relative_file. 810 * 811 * @return true|false 812 */ 813 public function supports_relative_file() { 814 return $this->get_option('relativefiles'); 815 } 816 } 817 818 /** 819 * Generates and sends the thumbnail for an image in filesystem. 820 * 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 829 */ 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; 835 } 836 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); 843 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))); 852 } 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; 857 } 858 send_stored_file($file, $lifetime, 0, $forcedownload, $options); 859 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body