ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç db/tasks.php000064400000002256152266405210006777 0ustar00. /** * Definition of file system repository scheduled tasks. * * @package repository_filesystem * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\repository_filesystem\task\cron_task', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ) ); db/access.php000064400000002427152266405210007113 0ustar00. /** * Plugin capabilities. * * @package repository_filesystem * @copyright 2009 Dongsheng Cai * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'repository/filesystem:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'coursecreator' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); lang/en/repository_filesystem.php000064400000004431152266405210013270 0ustar00. /** * Strings for component 'repository_filesystem', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_filesystem * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['blockedpath'] = 'Blocked root path'; $string['configplugin'] = 'Configuration for file system repository'; $string['crontask'] = 'Background processing for file system repository'; $string['donotusesysdir'] = 'Don\'t use system directory as repository, including'; $string['filesystem:view'] = 'View file system repository'; $string['information'] = 'These folders are within the {$a} directory.'; $string['invalidpath'] = 'Invalid root path'; $string['path'] = 'Select a subdirectory'; $string['relativefiles'] = 'Allow relative files'; $string['relativefiles_desc'] = 'This allows all files in the repository to be accessible using relative links.'; $string['root'] = 'Root'; $string['nosubdir'] = 'You need to create at least one folder inside the {$a} directory so you can select it here.'; $string['pluginname_help'] = 'Create repository from local directory'; $string['pluginname'] = 'File system'; $string['searchresults'] = 'Search results'; $string['enablecourseinstances'] = 'Allow admins to add a file system repository instance to a course (configurable only by admins)'; $string['enableuserinstances'] = 'Allow admins to add a file system repository instance for personal use (configurable only by admins)'; $string['privacy:metadata'] = 'The File system repository plugin does not store or transmit any personal data.'; pix/icon.png000064400000000652152266405210007010 0ustar00PNG  IHDR(-SPLTEnpknpk KtRNS+NIDATxeυUDA /!<-3q۽\F)?ˉD5MP. /** * This plugin is used to access files on server file system * * @since Moodle 2.0 * @package repository_filesystem * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/filelib.php'); /** * repository_filesystem class * * Create a repository from your local filesystem * *NOTE* for security issue, we use a fixed repository path * which is %moodledata%/repository * * @package repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_filesystem extends repository { /** * The subdirectory of the instance. * * @var string */ protected $subdir; /** * Constructor * * @param int $repositoryid repository ID * @param int $context context ID * @param array $options */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) { parent::__construct($repositoryid, $context, $options); $this->subdir = $this->get_option('fs_path'); } /** * Get the list of files and directories in that repository. * * @param string $fullpath Path to explore. This is assembled by {@link self::build_node_path()}. * @param string $page Page number. * @return array List of files and folders. */ public function get_listing($fullpath = '', $page = '') { global $OUTPUT; $list = array( 'list' => array(), 'manage' => false, 'dynload' => true, 'nologin' => true, 'path' => array() ); // We analyse the path to extract what to browse. $fullpath = empty($fullpath) ? $this->build_node_path('root') : $fullpath; $trail = explode('|', $fullpath); $trail = array_pop($trail); list($mode, $path, $unused) = $this->explode_node_path($trail); // Is that a search? if ($mode === 'search') { return $this->search($path, $page); } // Cleaning up the requested path. $path = trim($path, '/'); if (!$this->is_in_repository($path)) { // In case of doubt on the path, reset to default. $path = ''; } $rootpath = $this->get_rootpath(); $abspath = rtrim($rootpath . $path, '/') . '/'; // Retrieve list of files and directories and sort them. $fileslist = array(); $dirslist = array(); if ($dh = opendir($abspath)) { while (false !== ($file = readdir($dh))) { if ($file != '.' and $file != '..') { if (is_file($abspath . $file)) { $fileslist[] = $file; } else { $dirslist[] = $file; } } } closedir($dh); } core_collator::asort($fileslist, core_collator::SORT_NATURAL); core_collator::asort($dirslist, core_collator::SORT_NATURAL); // Fill the results. foreach ($dirslist as $file) { $list['list'][] = $this->build_node($rootpath, $path, $file, true, $fullpath); } foreach ($fileslist as $file) { $list['list'][] = $this->build_node($rootpath, $path, $file, false, $fullpath); } $list['path'] = $this->build_breadcrumb($fullpath); $list['list'] = array_filter($list['list'], array($this, 'filter')); return $list; } /** * Search files in repository. * * This search works by walking through the directories returning the files that match. Once * the limit of files is reached the walk stops. Whenever more files are requested, the walk * starts from the beginning until it reaches an additional set of files to return. * * @param string $query The query string. * @param int $page The page number. * @return mixed */ public function search($query, $page = 1) { global $OUTPUT, $SESSION; $query = core_text::strtolower($query); $remainingdirs = 1000; $remainingobjects = 5000; $perpage = 50; // Because the repository API is weird, the first page is 0, but it should be 1. if (!$page) { $page = 1; } // Initialise the session variable in which we store the search related things. if (!isset($SESSION->repository_filesystem_search)) { $SESSION->repository_filesystem_search = array(); } // Restore, or initialise the session search variables. if ($page <= 1) { $SESSION->repository_filesystem_search['query'] = $query; $SESSION->repository_filesystem_search['from'] = 0; $from = 0; } else { // Yes, the repository does not send the query again... $query = $SESSION->repository_filesystem_search['query']; $from = (int) $SESSION->repository_filesystem_search['from']; } $limit = $from + $perpage; $searchpath = $this->build_node_path('search', $query); // Pre-search initialisation. $rootpath = $this->get_rootpath(); $found = 0; $toexplore = array(''); // Retrieve list of matching files and directories. $matches = array(); while (($path = array_shift($toexplore)) !== null) { $remainingdirs--; if ($objects = scandir($rootpath . $path)) { foreach ($objects as $object) { $objectabspath = $rootpath . $path . $object; if ($object == '.' || $object == '..') { continue; } $remainingobjects--; $isdir = is_dir($objectabspath); // It is a match! if (strpos(core_text::strtolower($object), $query) !== false) { $found++; $matches[] = array($path, $object, $isdir); // That's enough, no need to find more. if ($found >= $limit) { break 2; } } // I've seen enough files, I give up! if ($remainingobjects <= 0) { break 2; } // Add the directory to things to explore later. if ($isdir) { $toexplore[] = $path . trim($object, '/') . '/'; } } } if ($remainingdirs <= 0) { break; } } // Extract the results from all the matches. $matches = array_slice($matches, $from, $perpage); // If we didn't reach our limits of browsing, and we appear to still have files to find. if ($remainingdirs > 0 && $remainingobjects > 0 && count($matches) >= $perpage) { $SESSION->repository_filesystem_search['from'] = $limit; $pages = -1; // We reached the end of the repository, or our limits. } else { $SESSION->repository_filesystem_search['from'] = 0; $pages = 0; } // Organise the nodes. $results = array(); foreach ($matches as $match) { list($path, $name, $isdir) = $match; $results[] = $this->build_node($rootpath, $path, $name, $isdir, $searchpath); } $list = array(); $list['list'] = array_filter($results, array($this, 'filter')); $list['dynload'] = true; $list['nologin'] = true; $list['page'] = $page; $list['pages'] = $pages; $list['path'] = $this->build_breadcrumb($searchpath); return $list; } /** * Build the breadcrumb from a full path. * * @param string $path A path generated by {@link self::build_node_path()}. * @return array */ protected function build_breadcrumb($path) { $breadcrumb = array(array( 'name' => get_string('root', 'repository_filesystem'), 'path' => $this->build_node_path('root') )); $crumbs = explode('|', $path); $trail = ''; foreach ($crumbs as $crumb) { list($mode, $nodepath, $display) = $this->explode_node_path($crumb); switch ($mode) { case 'search': $breadcrumb[] = array( 'name' => get_string('searchresults', 'repository_filesystem'), 'path' => $this->build_node_path($mode, $nodepath, $display, $trail), ); break; case 'browse': $breadcrumb[] = array( 'name' => $display, 'path' => $this->build_node_path($mode, $nodepath, $display, $trail), ); break; } $lastcrumb = end($breadcrumb); $trail = $lastcrumb['path']; } return $breadcrumb; } /** * Build a file or directory node. * * @param string $rootpath The absolute path to the repository. * @param string $path The relative path of the object * @param string $name The name of the object * @param string $isdir Is the object a directory? * @param string $rootnodepath The node leading to this node (for breadcrumb). * @return array */ protected function build_node($rootpath, $path, $name, $isdir, $rootnodepath) { global $OUTPUT; $relpath = trim($path, '/') . '/' . $name; $abspath = $rootpath . $relpath; $node = array( 'title' => $name, 'datecreated' => filectime($abspath), 'datemodified' => filemtime($abspath), ); if ($isdir) { $node['children'] = array(); $node['thumbnail'] = $OUTPUT->image_url(file_folder_icon())->out(false); $node['path'] = $this->build_node_path('browse', $relpath, $name, $rootnodepath); } else { $node['source'] = $relpath; $node['size'] = filesize($abspath); $node['thumbnail'] = $OUTPUT->image_url(file_extension_icon($name))->out(false); $node['icon'] = $OUTPUT->image_url(file_extension_icon($name))->out(false); $node['path'] = $relpath; if (file_extension_in_typegroup($name, 'image') && ($imageinfo = @getimagesize($abspath))) { // This means it is an image and we can return dimensions and try to generate thumbnail/icon. $token = $node['datemodified'] . $node['size']; // To prevent caching by browser. $node['realthumbnail'] = $this->get_thumbnail_url($relpath, 'thumb', $token)->out(false); $node['realicon'] = $this->get_thumbnail_url($relpath, 'icon', $token)->out(false); $node['image_width'] = $imageinfo[0]; $node['image_height'] = $imageinfo[1]; } } return $node; } /** * Build the path to a browsable node. * * @param string $mode The type of browse mode. * @param string $realpath The path, or similar. * @param string $display The way to display the node. * @param string $root The path preceding this node. * @return string */ protected function build_node_path($mode, $realpath = '', $display = '', $root = '') { $path = $mode . ':' . base64_encode($realpath) . ':' . base64_encode($display); if (!empty($root)) { $path = $root . '|' . $path; } return $path; } /** * Extract information from a node path. * * Note, this should not include preceding paths. * * @param string $path The path of the node. * @return array Contains the mode, the relative path, and the display text. */ protected function explode_node_path($path) { list($mode, $realpath, $display) = explode(':', $path); return array( $mode, base64_decode($realpath), base64_decode($display) ); } /** * To check whether the user is logged in. * * @return bool */ public function check_login() { return true; } /** * Show the login screen, if required. * * @return string */ public function print_login() { return true; } /** * Is it possible to do a global search? * * @return bool */ public function global_search() { return false; } /** * Return file path. * @return array */ public function get_file($file, $title = '') { global $CFG; $file = ltrim($file, '/'); if (!$this->is_in_repository($file)) { throw new repository_exception('Invalid file requested.'); } $file = $this->get_rootpath() . $file; // This is a hack to prevent move_to_file deleting files in local repository. $CFG->repository_no_delete = true; return array('path' => $file, 'url' => ''); } /** * Return the source information * * @param stdClass $filepath * @return string|null */ public function get_file_source_info($filepath) { return $filepath; } /** * Logout from repository instance * * @return string */ public function logout() { return true; } /** * Return names of the instance options. * * @return array */ public static function get_instance_option_names() { return array('fs_path', 'relativefiles'); } /** * Save settings for repository instance * * @param array $options settings * @return bool */ public function set_option($options = array()) { $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH); $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT); $ret = parent::set_option($options); return $ret; } /** * Edit/Create Instance Settings Moodle form * * @param moodleform $mform Moodle form (passed by reference) */ public static function instance_config_form($mform) { global $CFG; if (has_capability('moodle/site:config', context_system::instance())) { $path = $CFG->dataroot . '/repository/'; if (!is_dir($path)) { mkdir($path, $CFG->directorypermissions, true); } if ($handle = opendir($path)) { $fieldname = get_string('path', 'repository_filesystem'); $choices = array(); while (false !== ($file = readdir($handle))) { if (is_dir($path . $file) && $file != '.' && $file != '..') { $choices[$file] = $file; $fieldname = ''; } } if (empty($choices)) { $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path)); $mform->addElement('hidden', 'fs_path', ''); $mform->setType('fs_path', PARAM_PATH); } else { $mform->addElement('select', 'fs_path', $fieldname, $choices); $mform->addElement('static', null, '', get_string('information', 'repository_filesystem', $path)); } closedir($handle); } $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'), get_string('relativefiles_desc', 'repository_filesystem')); $mform->setType('relativefiles', PARAM_INT); } else { $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin', 'repository_filesystem'))); return false; } } /** * Create an instance for this plug-in * * @static * @param string $type the type of the repository * @param int $userid the user id * @param stdClass $context the context * @param array $params the options for this instance * @param int $readonly whether to create it readonly or not (defaults to not) * @return mixed */ public static function create($type, $userid, $context, $params, $readonly=0) { if (has_capability('moodle/site:config', context_system::instance())) { return parent::create($type, $userid, $context, $params, $readonly); } else { require_capability('moodle/site:config', context_system::instance()); return false; } } /** * Validate repository plugin instance form * * @param moodleform $mform moodle form * @param array $data form data * @param array $errors errors * @return array errors */ public static function instance_form_validation($mform, $data, $errors) { $fspath = clean_param(trim($data['fs_path'], '/'), PARAM_PATH); if (empty($fspath) && !is_numeric($fspath)) { $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path'); } return $errors; } /** * User cannot use the external link to dropbox * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Return human readable reference information * * @param string $reference value of DB field files_reference.reference * @param int $filestatus status of the file, 0 - ok, 666 - source missing * @return string */ public function get_reference_details($reference, $filestatus = 0) { $details = $this->get_name().': '.$reference; if ($filestatus) { return get_string('lostsource', 'repository', $details); } else { return $details; } } public function sync_reference(stored_file $file) { if ($file->get_referencelastsync() + 60 > time()) { // Does not cost us much to synchronise within our own filesystem, check every 1 minute. return false; } static $issyncing = false; if ($issyncing) { // Avoid infinite recursion when calling $file->get_filesize() and get_contenthash(). return false; } $filepath = $this->get_rootpath() . ltrim($file->get_reference(), '/'); if ($this->is_in_repository($file->get_reference()) && file_exists($filepath) && is_readable($filepath)) { $fs = get_file_storage(); $issyncing = true; if (file_extension_in_typegroup($filepath, 'web_image')) { $contenthash = file_storage::hash_from_path($filepath); if ($file->get_contenthash() == $contenthash) { // File did not change since the last synchronisation. $filesize = filesize($filepath); } else { // Copy file into moodle filepool (used to generate an image thumbnail). $file->set_timemodified(filemtime($filepath)); $file->set_synchronised_content_from_file($filepath); return true; } } else { // Update only file size so file will NOT be copied into moodle filepool. if ($file->compare_to_string('') || !$file->compare_to_path($filepath)) { // File is not synchronized or the file has changed. $contenthash = file_storage::hash_from_string(''); } else { // File content was synchronised and has not changed since then, leave it. $contenthash = null; } $filesize = filesize($filepath); } $issyncing = false; $modified = filemtime($filepath); $file->set_synchronized($contenthash, $filesize, 0, $modified); } else { $file->set_missingsource(); } return true; } /** * Repository method to serve the referenced file * * @see send_stored_file * * @param stored_file $storedfile the file that contains the reference * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime) * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, ?array $options = null) { $reference = $storedfile->get_reference(); $file = $this->get_rootpath() . ltrim($reference, '/'); if ($this->is_in_repository($reference) && is_readable($file)) { $filename = $storedfile->get_filename(); if ($options && isset($options['filename'])) { $filename = $options['filename']; } $dontdie = ($options && isset($options['dontdie'])); // If the file is linked to the original then we should not cache it. $lifetime = 0; send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie); } else { send_file_not_found(); } } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } /** * Return the rootpath of this repository instance. * * Trim() is a necessary step to ensure that the subdirectory is not '/'. * * @return string path * @throws repository_exception If the subdir is unsafe, or invalid. */ public function get_rootpath() { global $CFG; $subdir = clean_param(trim($this->subdir, '/'), PARAM_PATH); $path = $CFG->dataroot . '/repository/' . $this->subdir . '/'; if ((empty($this->subdir) && !is_numeric($this->subdir)) || $subdir != $this->subdir || !is_dir($path)) { throw new repository_exception('The instance is not properly configured, invalid path.'); } return $path; } /** * Checks if $path is part of this repository. * * Try to prevent $path hacks such as ../ . * * We do not use clean_param(, PARAM_PATH) here because it also trims down some * characters that are allowed, like < > ' . But we do ensure that the directory * is safe by checking that it starts with $rootpath. * * @param string $path relative path to a file or directory in the repo. * @return boolean false when not. */ protected function is_in_repository($path) { $rootpath = $this->get_rootpath(); if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) { return false; } return true; } /** * Returns url of thumbnail file. * * @param string $filepath current path in repository (dir and filename) * @param string $thumbsize 'thumb' or 'icon' * @param string $token identifier of the file contents - to prevent browser from caching changed file * @return moodle_url */ protected function get_thumbnail_url($filepath, $thumbsize, $token) { return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id, '/' . trim($filepath, '/') . '/', $token); } /** * Returns the stored thumbnail file, generates it if not present. * * @param string $filepath current path in repository (dir and filename) * @param string $thumbsize 'thumb' or 'icon' * @return null|stored_file */ public function get_thumbnail($filepath, $thumbsize) { global $CFG; $filepath = trim($filepath, '/'); $origfile = $this->get_rootpath() . $filepath; // As thumbnail filename we use original file content hash. if (!$this->is_in_repository($filepath) || !($filecontents = @file_get_contents($origfile))) { // File is not found or is not readable. return null; } $filename = file_storage::hash_from_string($filecontents); // Try to get generated thumbnail for this file. $fs = get_file_storage(); if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/', $filename))) { // Thumbnail not found . Generate and store thumbnail. require_once($CFG->libdir . '/gdlib.php'); if ($thumbsize === 'thumb') { $size = 90; } else { $size = 24; } if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) { // Generation failed. return null; } $record = array( 'contextid' => SYSCONTEXTID, 'component' => 'repository_filesystem', 'filearea' => $thumbsize, 'itemid' => $this->id, 'filepath' => '/' . $filepath . '/', 'filename' => $filename, ); $file = $fs->create_file_from_string($record, $data); } return $file; } /** * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files. * * @param stored_file[] $storedfiles */ public function remove_obsolete_thumbnails($storedfiles) { // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository). $files = array(); foreach ($storedfiles as $file) { if (!isset($files[$file->get_filepath()])) { $files[$file->get_filepath()] = array(); } $files[$file->get_filepath()][] = $file; } // Loop through all files and make sure the original exists and has the same contenthash. $deletedcount = 0; foreach ($files as $filepath => $filesinpath) { if ($filecontents = @file_get_contents($this->get_rootpath() . trim($filepath, '/'))) { // The 'filename' in Moodle file storage is contenthash of the file in filesystem repository. $filename = file_storage::hash_from_string($filecontents); foreach ($filesinpath as $file) { if ($file->get_filename() !== $filename && $file->get_filename() !== '.') { // Contenthash does not match, this is an old thumbnail. $deletedcount++; $file->delete(); } } } else { // Thumbnail exist but file not. foreach ($filesinpath as $file) { if ($file->get_filename() !== '.') { $deletedcount++; } $file->delete(); } } } if ($deletedcount) { mtrace(" instance {$this->id}: deleted $deletedcount thumbnails"); } } /** * Gets a file relative to this file in the repository and sends it to the browser. * * @param stored_file $mainfile The main file we are trying to access relative files for. * @param string $relativepath the relative path to the file we are trying to access. */ public function send_relative_file(stored_file $mainfile, $relativepath) { global $CFG; // Check if this repository is allowed to use relative linking. $allowlinks = $this->supports_relative_file(); if (!empty($allowlinks)) { // Get path to the mainfile. $mainfilepath = $mainfile->get_source(); // Strip out filename from the path. $filename = $mainfile->get_filename(); $basepath = strstr($mainfilepath, $filename, true); $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath); // Sanity check to make sure this path is inside this repository and the file exists. if (strpos($fullrelativefilepath, realpath($this->get_rootpath())) === 0 && file_exists($fullrelativefilepath)) { send_file($fullrelativefilepath, basename($relativepath), null, 0); } } send_file_not_found(); } /** * helper function to check if the repository supports send_relative_file. * * @return true|false */ public function supports_relative_file() { return $this->get_option('relativefiles'); } } /** * Generates and sends the thumbnail for an image in filesystem. * * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool */ function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { global $OUTPUT, $CFG; // Allowed filearea is either thumb or icon - size of the thumbnail. if ($filearea !== 'thumb' && $filearea !== 'icon') { return false; } // As itemid we pass repository instance id. $itemid = array_shift($args); // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed). array_pop($args); // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository. $filepath = implode('/', $args); // Make sure file exists in the repository and is accessible. $repo = repository::get_repository_by_id($itemid, $context); $repo->check_capability(); // Find stored or generated thumbnail. if (!($file = $repo->get_thumbnail($filepath, $filearea))) { // Generation failed, redirect to default icon for file extension. // Do not use redirect() here because is not compatible with webservice/pluginfile.php. header('Location: ' . $OUTPUT->image_url(file_extension_icon($file))); } // The thumbnails should not be changing much, but maybe the default lifetime is too long. $lifetime = $CFG->filelifetime; if ($lifetime > 60*10) { $lifetime = 60*10; } send_stored_file($file, $lifetime, 0, $forcedownload, $options); } version.php000064400000002314152266405210006745 0ustar00. /** * Version details * * @package repository * @subpackage filesystem * @copyright 2009 Dongsheng Cai * @author Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'repository_filesystem'; // Full name of the plugin (used for diagnostics) classes/privacy/provider.php000064400000003004152266405210012221 0ustar00. /** * Privacy Subsystem implementation for repository_filesystem. * * @package repository_filesystem * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_filesystem\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_filesystem implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } classes/task/cron_task.php000064400000005573152266405210011654 0ustar00. namespace repository_filesystem\task; defined('MOODLE_INTERNAL') || die(); /** * A schedule task for file system repository cron. * * @package repository_filesystem * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cron_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('crontask', 'repository_filesystem'); } /** * Run file system repository cron. */ public function execute() { global $CFG; require_once($CFG->dirroot . '/repository/lib.php'); $fs = get_file_storage(); // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id). $allfiles = array_merge( $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'), $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon') ); $filesbyitem = array(); foreach ($allfiles as $file) { if (!isset($filesbyitem[$file->get_itemid()])) { $filesbyitem[$file->get_itemid()] = array(); } $filesbyitem[$file->get_itemid()][] = $file; } // Find all instances of repository_filesystem. $instances = \repository::get_instances(array('type' => 'filesystem')); // Loop through all itemids of generated thumbnails. foreach ($filesbyitem as $itemid => $files) { if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) { // Instance was deleted. $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid); $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid); mtrace(" instance $itemid does not exist: deleted all thumbnails"); } else { // Instance has some generated thumbnails, check that they are not outdated. $instances[$itemid]->remove_obsolete_thumbnails($files); } } } } tests/generator/lib.php000064400000003176152266405210011165 0ustar00. /** * File system repository data generator * * @package repository_filesystem * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * File system repository data generator class * * @package repository_filesystem * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_filesystem_generator extends testing_repository_generator { /** * Fill in record defaults. * * @param array $record * @return array */ protected function prepare_record(array $record) { $record = parent::prepare_record($record); if (!isset($record['fs_path'])) { $record['fs_path'] = '/i/do/not/exist'; } if (!isset($record['relativefiles'])) { $record['relativefiles'] = 0; } return $record; } } tests/repository_filesystem_test.php000064400000003536152266405210014153 0ustar00. /** * Tests for the repository_filesystem plugin. * * @package repository_filesystem * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \repository_filesystem */ final class repository_filesystem_test extends \advanced_testcase { public function test_get_listing(): void { global $CFG; $this->resetAfterTest(true); $user = get_admin(); $this->setUser($user); $this->getDataGenerator()->create_repository_type('filesystem'); mkdir($CFG->dataroot . '/repository/test/0', recursive: true); file_put_contents($CFG->dataroot . '/repository/test/0/test.txt', 'test'); $record = $this->getDataGenerator()->create_repository('filesystem', [ 'fs_path' => 'test', ]); /** @var repository_filesystem $repository */ $repository = repository::get_repository_by_id($record->id, \core\context\user::instance($user->id)); $listing = $repository->get_listing(); $this->assertCount(1, $listing['list']); $this->assertEquals(['0'], array_column($listing['list'], 'title')); } }