You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
860 lines
31 KiB
860 lines
31 KiB
2 years ago
|
<?php
|
||
|
// This file is part of Moodle - http://moodle.org/
|
||
|
//
|
||
|
// Moodle is free software: you can redistribute it and/or modify
|
||
|
// it under the terms of the GNU General Public License as published by
|
||
|
// the Free Software Foundation, either version 3 of the License, or
|
||
|
// (at your option) any later version.
|
||
|
//
|
||
|
// Moodle is distributed in the hope that it will be useful,
|
||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
// GNU General Public License for more details.
|
||
|
//
|
||
|
// You should have received a copy of the GNU General Public License
|
||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
/**
|
||
|
* 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 (($file = readdir($dh)) != false) {
|
||
|
if ($file != '.' and $file != '..') {
|
||
|
if (is_file($abspath . $file)) {
|
||
|
$fileslist[] = $file;
|
||
|
} else {
|
||
|
$dirslist[] = $file;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
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(90))->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, 90))->out(false);
|
||
|
$node['icon'] = $OUTPUT->image_url(file_extension_icon($name, 24))->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']));
|
||
|
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, 90)));
|
||
|
}
|
||
|
// 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);
|
||
|
}
|