Skip to content

Commit

Permalink
MDL-58132 repositories: Controlled link file type
Browse files Browse the repository at this point in the history
This introduces a new "controlled link" file type where the file is not
stored in Moodle - but Moodle will control the access permissions on the file.

Plugins can "freeze" a filearea which means Moodle will take ownership of all the remote
files of this type.

When accessing a file, if the "filebrowser" infomation indicates the current user can write to the file, they
will be granted temporary write access.

Part of MDL-58220
  • Loading branch information
Damyon Wiese committed Apr 3, 2017
1 parent 8ece1d7 commit 151b0f9
Show file tree
Hide file tree
Showing 29 changed files with 391 additions and 44 deletions.
3 changes: 2 additions & 1 deletion backup/backupfilesedit_form.php
Expand Up @@ -30,7 +30,8 @@ class backup_files_edit_form extends moodleform {
public function definition() {
$mform =& $this->_form;

$options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => FILE_INTERNAL | FILE_REFERENCE);
$types = (FILE_INTERNAL | FILE_REFERENCE | FILE_CONTRLLED_LINK);
$options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => $types);

$mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options);

Expand Down
6 changes: 6 additions & 0 deletions files/renderer.php
Expand Up @@ -772,6 +772,12 @@ protected function fp_js_template_selectlayout() {
<input type="radio"/>
</div>
</div>
<div class="fp-linktype-8 control-group control-radio clearfix">
<label class="control-label control-radio">'.get_string('makefilecontrolledlink', 'repository').'</label>
<div class="controls control-radio">
<input type="radio"/>
</div>
</div>
<div class="fp-saveas control-group clearfix">
<label class="control-label">'.get_string('saveas', 'repository').'</label>
<div class="controls">
Expand Down
1 change: 1 addition & 0 deletions lang/en/repository.php
Expand Up @@ -160,6 +160,7 @@
$string['makefileinternal'] = 'Make a copy of the file';
$string['makefilelink'] = 'Link to the file directly';
$string['makefilereference'] = 'Create an alias/shortcut to the file';
$string['makefilecontrolledlink'] = 'Create an access controlled link to the file';
$string['manage'] = 'Manage repositories';
$string['manageinstances'] = 'Manage instances';
$string['manageurl'] = 'Manage';
Expand Down
17 changes: 16 additions & 1 deletion lib/filelib.php
Expand Up @@ -256,6 +256,21 @@ function file_postupdate_standard_editor($data, $field, array $options, $context
return $data;
}

/**
* For all files in this file area - walk the file list and copy each to a system owned account, making them read-only.
*
* @category files
* @param stdClass $context context - must already exist
* @param string $component
* @param string $filearea file area name
* @param int $itemid
* @return bool
*/
function file_prevent_changes_to_external_files($contextid, $component, $filearea, $itemid=false) {
$fs = get_file_storage();
return $fs->prevent_changes_to_external_files($contextid, $component, $filearea, $itemid);
}

/**
* Saves text and files modified by Editor formslib element
*
Expand Down Expand Up @@ -813,7 +828,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
$options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED; // Unlimited.
}
$allowreferences = true;
if (isset($options['return_types']) && !($options['return_types'] & FILE_REFERENCE)) {
if (isset($options['return_types']) && !($options['return_types'] & (FILE_REFERENCE | FILE_CONTROLLED_LINK))) {
// we assume that if $options['return_types'] is NOT specified, we DO allow references.
// this is not exactly right. BUT there are many places in code where filemanager options
// are not passed to file_save_draft_area_files()
Expand Down
31 changes: 31 additions & 0 deletions lib/filestorage/file_storage.php
Expand Up @@ -2323,4 +2323,35 @@ public function update_references($referencefileid, $lastsync, $lifetime, $conte
$data = array('id' => $referencefileid, 'lastsync' => $lastsync);
$DB->update_record('files_reference', (object)$data);
}

/**
* For an entire file area - walk through the files and for each one that is a controlled link,
* call prevent_changes on the repository. Typically this will copy the external file to a system
* account controlled by Moodle, remove all write access and update the file reference.
*
* @param int $contextid
* @param string $component
* @param string $filearea
* @param int $itemid
*/
public function prevent_changes_to_external_files($contextid, $component, $filearea, $itemid = false) {
global $DB;

$transaction = $DB->start_delegated_transaction();

$files = $this->get_area_files($contextid, $component, $filearea, $itemid, 'id', false);

foreach ($files as $file) {
if ($file->is_external_file()) {
// Note that this function uses a cache, so we don't need to
// double cache these.
$repo = repository::get_repository_by_id($file->get_repository_id(), SYSCONTEXTID);

// We expect this function to throw exceptions on failure.
$repo->prevent_changes_to_external_file($file);
}
}
$transaction->allow_commit();
return true;
}
}
9 changes: 9 additions & 0 deletions lib/filestorage/stored_file.php
Expand Up @@ -102,6 +102,15 @@ public function is_external_file() {
return !empty($this->repository);
}

/**
* Whether or not this is a controlled link. Note that repositories cannot support FILE_REFERENCE and FILE_CONTROLLED_LINK.
*
* @return bool
*/
public function is_controlled_link() {
return $this->is_external_file() && $this->repository->supported_returntypes() & FILE_CONTROLLED_LINK;
}

/**
* Update some file record fields
* NOTE: Must remain protected
Expand Down
4 changes: 2 additions & 2 deletions lib/form/editor.php
Expand Up @@ -58,8 +58,8 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
/** @var array options provided to initalize filepicker */
protected $_options = array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 0, 'changeformat' => 0,
'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED, 'context' => null, 'noclean' => 0, 'trusttext' => 0,
'return_types' => 7, 'enable_filemanagement' => true);
// $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE
'return_types' => 15, 'enable_filemanagement' => true);
// $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK

/** @var array values for editor */
protected $_values = array('text'=>null, 'format'=>null, 'itemid'=>null);
Expand Down
2 changes: 1 addition & 1 deletion lib/form/filemanager.php
Expand Up @@ -78,7 +78,7 @@ public function __construct($elementName=null, $elementLabel=null, $attributes=n
$this->_options['maxbytes'] = get_user_max_upload_file_size($PAGE->context, $CFG->maxbytes, $options['maxbytes']);
}
if (empty($options['return_types'])) {
$this->_options['return_types'] = (FILE_INTERNAL | FILE_REFERENCE);
$this->_options['return_types'] = (FILE_INTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK);
}
$this->_type = 'filemanager';
parent::__construct($elementName, $elementLabel, $attributes);
Expand Down
5 changes: 5 additions & 0 deletions lib/upgrade.txt
@@ -1,6 +1,11 @@
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
=== 3.3 ===
* Support added for a new type of external file: FILE_CONTROLLED_LINK. This is an external file that Moodle can control
the permissions. Moodle can make files read-only or grant temporary write access.
To make all the files in file area read only (owned by Moodle) - use file_prevent_changes_to_external_files().
When accessing a URL, the info from file_browser::get_file_info will be checked to determine if the user has write access,
if they do - the remote file will have access controls set to allow editing.
* The method moodleform::after_definition() has been added and can now be used to add some logic
to be performed after the form's definition was set. This is useful for intermediate subclasses.
* Moodle has support for font-awesome icons. Plugins should use the xxx_get_fontawesome_icon_map callback
Expand Down
20 changes: 15 additions & 5 deletions mod/assign/assignmentplugin.php
Expand Up @@ -580,17 +580,26 @@ public function get_file_areas() {
public function get_file_info($browser, $filearea, $itemid, $filepath, $filename) {
global $CFG, $DB, $USER;
$urlbase = $CFG->wwwroot.'/pluginfile.php';

$writeaccess = false;
// Permission check on the itemid.

if ($this->get_subtype() == 'assignsubmission') {
if ($itemid) {
$record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid', IGNORE_MISSING);
$record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid,groupid', IGNORE_MISSING);
if (!$record) {
return null;
}
if (!$this->assignment->can_view_submission($record->userid)) {
return null;
if (!empty($record->userid)) {
if (!$this->assignment->can_view_submission($record->userid)) {
return null;
}
$writeaccess = $this->assignment->can_edit_submission($record->userid);
} else {
// Must be a team submission with a group.
if (!$this->assignment->can_view_group_submission($record->groupid)) {
return null;
}
$writeaccess = $this->assignment->can_edit_group_submission($record->groupid);
}
}
} else {
Expand All @@ -609,14 +618,15 @@ public function get_file_info($browser, $filearea, $itemid, $filepath, $filename
$filename))) {
return null;
}

return new file_info_stored($browser,
$this->assignment->get_context(),
$storedfile,
$urlbase,
$filearea,
$itemid,
true,
true,
$writeaccess,
false);
}

Expand Down
1 change: 1 addition & 0 deletions mod/assign/lang/en/assign.php
Expand Up @@ -155,6 +155,7 @@
$string['duedatereached'] = 'The due date for this assignment has now passed';
$string['duedatevalidation'] = 'Due date must be after the allow submissions from date.';
$string['editattemptfeedback'] = 'Edit the grade and feedback for attempt number {$a}.';
$string['editonline'] = 'Edit online';
$string['editingpreviousfeedbackwarning'] = 'You are editing the feedback for a previous attempt. This is attempt {$a->attemptnumber} out of {$a->totalattempts}.';
$string['editoverride'] = 'Edit override';
$string['editsubmission'] = 'Edit submission';
Expand Down
19 changes: 19 additions & 0 deletions mod/assign/locallib.php
Expand Up @@ -4360,6 +4360,25 @@ protected function is_graded($userid) {
return false;
}

/**
* Perform an access check to see if the current $USER can edit this group submission.
*
* @param int $groupid
* @return bool
*/
public function can_edit_group_submission($groupid) {
global $USER;

$members = $this->get_submission_group_members($groupid, true);
foreach ($members as $member) {
// If we can edit any members submission, we can edit the submission for the group.
if ($this->can_edit_submission($member->id)) {
return true;
}
}
return false;
}

/**
* Perform an access check to see if the current $USER can view this group submission.
*
Expand Down
1 change: 1 addition & 0 deletions mod/assign/renderable.php
Expand Up @@ -911,6 +911,7 @@ public function __construct(context $context, $sid, $filearea, $component) {
*/
public function preprocess($dir, $filearea, $component) {
global $CFG;

foreach ($dir['subdirs'] as $subdir) {
$this->preprocess($subdir, $filearea, $component);
}
Expand Down
2 changes: 1 addition & 1 deletion mod/assign/renderer.php
Expand Up @@ -1420,7 +1420,7 @@ protected function htmllize_tree(assign_files $tree, $dir) {
$result .= '<li yuiConfig=\'' . json_encode($yuiconfig) . '\'>' .
'<div>' . $image . ' ' .
$file->fileurl . ' ' .
$plagiarismlinks .
$plagiarismlinks . ' ' .
$file->portfoliobutton . '</div>' .
'</li>';
}
Expand Down
24 changes: 18 additions & 6 deletions mod/assign/submission/file/locallib.php
Expand Up @@ -125,11 +125,11 @@ public function save_settings(stdClass $data) {
* @return array
*/
private function get_file_options() {
$fileoptions = array('subdirs'=>1,
'maxbytes'=>$this->get_config('maxsubmissionsizebytes'),
'maxfiles'=>$this->get_config('maxfilesubmissions'),
'accepted_types'=>'*',
'return_types'=>FILE_INTERNAL);
$fileoptions = array('subdirs' => 1,
'maxbytes' => $this->get_config('maxsubmissionsizebytes'),
'maxfiles' => $this->get_config('maxfilesubmissions'),
'accepted_types' => '*',
'return_types' => (FILE_INTERNAL | FILE_CONTROLLED_LINK));
if ($fileoptions['maxbytes'] == 0) {
// Use module default.
$fileoptions['maxbytes'] = get_config('assignsubmission_file', 'maxbytes');
Expand Down Expand Up @@ -174,7 +174,6 @@ public function get_form_elements($submission, MoodleQuickForm $mform, stdClass
* @return int
*/
private function count_files($submissionid, $area) {

$fs = get_file_storage();
$files = $fs->get_area_files($this->assignment->get_context()->id,
'assignsubmission_file',
Expand Down Expand Up @@ -553,6 +552,19 @@ public function get_external_parameters() {
);
}

/**
* Make any controlled links in the submission area read-only for the student.
*
* @param stdClass $submission the assign_submission record being submitted.
* @return void
*/
public function submit_for_grading($submission) {
file_prevent_changes_to_external_files($this->assignment->get_context()->id,
'assignsubmission_file',
ASSIGNSUBMISSION_FILE_FILEAREA,
$submission->id);
}

/**
* Return the plugin configs for external functions.
*
Expand Down
2 changes: 1 addition & 1 deletion mod/assign/submission/onlinetext/locallib.php
Expand Up @@ -174,7 +174,7 @@ private function get_edit_options() {
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $this->assignment->get_course()->maxbytes,
'context' => $this->assignment->get_context(),
'return_types' => FILE_INTERNAL | FILE_EXTERNAL
'return_types' => (FILE_INTERNAL | FILE_EXTERNAL | FILE_CONTROLLED_LINK)
);
return $editoroptions;
}
Expand Down
4 changes: 3 additions & 1 deletion mod/data/field/file/field.class.php
Expand Up @@ -85,7 +85,7 @@ function display_add_field($recordid = 0, $formdata = null) {
$options->maxfiles = 1; // Limit to one file for the moment, this may be changed if requested as a feature in the future.
$options->itemid = $itemid;
$options->accepted_types = '*';
$options->return_types = FILE_INTERNAL;
$options->return_types = FILE_INTERNAL | FILE_CONTROLLED_LINK;
$options->context = $PAGE->context;

$fm = new form_filemanager($options);
Expand Down Expand Up @@ -185,6 +185,8 @@ function update_content($recordid, $value, $name='') {
$usercontext = context_user::instance($USER->id);
$files = $fs->get_area_files($this->context->id, 'mod_data', 'content', $content->id, 'itemid, filepath, filename', false);

file_prevent_changes_to_external_files($this->context->id, 'mod_data', 'content', $content->id);

// We expect no or just one file (maxfiles = 1 option is set for the form_filemanager).
if (count($files) == 0) {
$content->content = null;
Expand Down
2 changes: 1 addition & 1 deletion mod/forum/classes/post_form.php
Expand Up @@ -50,7 +50,7 @@ public static function attachment_options($forum) {
'maxbytes' => $maxbytes,
'maxfiles' => $forum->maxattachments,
'accepted_types' => '*',
'return_types' => FILE_INTERNAL
'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK
);
}

Expand Down
5 changes: 4 additions & 1 deletion mod/forum/lib.php
Expand Up @@ -559,13 +559,16 @@ function forum_cron() {
}
}

// We need to prevent changes to controlled links in attachments.
$modcontext = context_module::instance($coursemodules[$forumid]->id);
file_prevent_changes_to_external_files($modcontext->id, 'mod_forum', 'attachment', $pid);

// Save the Inbound Message datakey here to reduce DB queries later.
$messageinboundgenerator->set_data($pid);
$messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();

// Caching subscribed users of each forum.
if (!isset($subscribedusers[$forumid])) {
$modcontext = context_module::instance($coursemodules[$forumid]->id);
if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) {

foreach ($subusers as $postuser) {
Expand Down
3 changes: 2 additions & 1 deletion mod/wiki/filesedit.php
Expand Up @@ -83,7 +83,8 @@
$data->returnurl = $returnurl;
$data->subwikiid = $subwiki->id;
$maxbytes = get_max_upload_file_size($CFG->maxbytes, $COURSE->maxbytes);
$options = array('subdirs'=>0, 'maxbytes'=>$maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL | FILE_REFERENCE);
$types = FILE_INTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK;
$options = array('subdirs'=>0, 'maxbytes'=>$maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>$types);
file_prepare_standard_filemanager($data, 'files', $options, $context, 'mod_wiki', 'attachments', $subwiki->id);

$mform = new mod_wiki_filesedit_form(null, array('data'=>$data, 'options'=>$options));
Expand Down
11 changes: 9 additions & 2 deletions mod/workshop/locallib.php
Expand Up @@ -1843,6 +1843,13 @@ public function switch_phase($newphase) {
workshop_update_grades($workshop);
}

if (self::PHASE_ASSESSMENT == $newphase) {
file_prevent_changes_to_external_files($this->context, 'mod_workshop', 'submission_content');
}
if (self::PHASE_EVALUATION == $newphase) {
file_prevent_changes_to_external_files($this->context, 'mod_workshop', 'overallfeedback_attachment');
}

$DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
$this->phase = $newphase;
$eventdata = array(
Expand Down Expand Up @@ -2512,7 +2519,7 @@ public function submission_attachment_options() {
'subdirs' => true,
'maxfiles' => $this->nattachments,
'maxbytes' => $this->maxbytes,
'return_types' => FILE_INTERNAL,
'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK,
);

if ($acceptedtypes = self::normalize_file_extensions($this->submissionfiletypes)) {
Expand Down Expand Up @@ -2554,7 +2561,7 @@ public function overall_feedback_attachment_options() {
'subdirs' => 1,
'maxbytes' => $this->overallfeedbackmaxbytes,
'maxfiles' => $this->overallfeedbackfiles,
'return_types' => FILE_INTERNAL,
'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK,
);

if ($acceptedtypes = self::normalize_file_extensions($this->overallfeedbackfiletypes)) {
Expand Down
2 changes: 1 addition & 1 deletion question/type/essay/renderer.php
Expand Up @@ -115,7 +115,7 @@ public function files_input(question_attempt $qa, $numallowed,
$pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
'attachments', $options->context->id);
$pickeroptions->context = $options->context;
$pickeroptions->return_types = FILE_INTERNAL;
$pickeroptions->return_types = FILE_INTERNAL | FILE_CONTROLLED_LINK;

$pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
'attachments', $options->context->id);
Expand Down

0 comments on commit 151b0f9

Please sign in to comment.