From f3c2afd0e8e63368d6ecb94a068a355b926384dd Mon Sep 17 00:00:00 2001 From: Jake Dallimore Date: Thu, 27 Jul 2017 16:47:09 +0800 Subject: [PATCH 1/2] MDL-36985 core_files: added option to remove draft files on save text areas can now specify $options['removeorphaneddrafts'] when saving their data using file_postupdate_standard_editor(). If set to true, this option clears all user drafts which are not referenced in the text --- lib/filelib.php | 39 ++++++++++++++++ lib/tests/filelib_test.php | 45 +++++++++++++++++++ mod/assign/submission/onlinetext/locallib.php | 13 +++--- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/lib/filelib.php b/lib/filelib.php index d2779ced516..4b74d41e7a7 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -237,6 +237,9 @@ function file_postupdate_standard_editor($data, $field, array $options, $context if (!isset($options['maxbytes'])) { $options['maxbytes'] = 0; // unlimited } + if (!isset($options['removeorphaneddrafts'])) { + $options['removeorphaneddrafts'] = false; // Don't remove orphaned draft files by default. + } if ($options['trusttext']) { $data->{$field.'trust'} = trusttext_trusted($context); @@ -249,6 +252,10 @@ function file_postupdate_standard_editor($data, $field, array $options, $context if ($options['maxfiles'] == 0 or is_null($filearea) or is_null($itemid) or empty($editor['itemid'])) { $data->{$field} = $editor['text']; } else { + // Clean the user drafts area of any files not referenced in the editor text. + if ($options['removeorphaneddrafts']) { + file_remove_editor_orphaned_files($editor); + } $data->{$field} = file_save_draft_area_files($editor['itemid'], $context->id, $component, $filearea, $itemid, $options, $editor['text'], $options['forcehttps']); } $data->{$field.'format'} = $editor['format']; @@ -792,6 +799,38 @@ function file_restore_source_field_from_draft_file($storedfile) { } return $storedfile; } + +/** + * Removes those files from the user drafts filearea which are not referenced in the editor text. + * + * @param stdClass $editor The online text editor element from the submitted form data. + */ +function file_remove_editor_orphaned_files($editor) { + global $CFG, $USER; + + // Find those draft files included in the text, and generate their hashes. + $context = context_user::instance($USER->id); + $baseurl = $CFG->wwwroot . '/draftfile.php/' . $context->id . '/user/draft/' . $editor['itemid'] . '/'; + $pattern = "/" . preg_quote($baseurl, '/') . "(.+?)[\?\"']/"; + preg_match_all($pattern, $editor['text'], $matches); + $usedfilehashes = []; + foreach ($matches[1] as $matchedfilename) { + $matchedfilename = urldecode($matchedfilename); + $usedfilehashes[] = \file_storage::get_pathname_hash($context->id, 'user', 'draft', $editor['itemid'], '/', + $matchedfilename); + } + + // Now, compare the hashes of all draft files, and remove those which don't match used files. + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'user', 'draft', $editor['itemid'], 'id', false); + foreach ($files as $file) { + $tmphash = $file->get_pathnamehash(); + if (!in_array($tmphash, $usedfilehashes)) { + $file->delete(); + } + } +} + /** * Saves files from a draft file area to a real one (merging the list of files). * Can rewrite URLs in some content at the same time if desired. diff --git a/lib/tests/filelib_test.php b/lib/tests/filelib_test.php index 87a37bd0fe2..cb40c8fe7e8 100644 --- a/lib/tests/filelib_test.php +++ b/lib/tests/filelib_test.php @@ -1340,6 +1340,51 @@ EOF; $this->assertEquals(0, $fileinfo['foldercount']); // No subdirectories inside the directory. $this->assertEquals($file->get_filesize(), $fileinfo['filesize_without_references']); } + + /** + * Test confirming that draft files not referenced in the editor text are removed. + */ + public function test_file_remove_editor_orphaned_files() { + global $USER, $CFG; + $this->resetAfterTest(true); + $this->setAdminUser(); + + // Create three draft files. + $filerecord = ['filename' => 'file1.png']; + $file = self::create_draft_file($filerecord); + $draftitemid = $file->get_itemid(); + + $filerecord['itemid'] = $draftitemid; + + $filerecord['filename'] = 'file2.png'; + self::create_draft_file($filerecord); + + $filerecord['filename'] = 'file 3.png'; + self::create_draft_file($filerecord); + + // Confirm the user drafts area lists 3 files. + $fs = get_file_storage(); + $usercontext = context_user::instance($USER->id); + $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'itemid', 0); + $this->assertCount(3, $draftfiles); + + // Now, spoof some editor text content, referencing 2 of the files; one requiring name encoding, one not. + $editor = [ + 'itemid' => $draftitemid, + 'text' => ' + + ' + ]; + + // Run the remove orphaned drafts function and confirm that only the referenced files remain in the user drafts. + $expected = ['file1.png', 'file 3.png']; // The drafts we expect will not be removed (are referenced in the online text). + file_remove_editor_orphaned_files($editor); + $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'itemid', 0); + $this->assertCount(2, $draftfiles); + foreach ($draftfiles as $file) { + $this->assertContains($file->get_filename(), $expected); + } + } } /** diff --git a/mod/assign/submission/onlinetext/locallib.php b/mod/assign/submission/onlinetext/locallib.php index 5ec7c6d0e08..17e342b7a13 100644 --- a/mod/assign/submission/onlinetext/locallib.php +++ b/mod/assign/submission/onlinetext/locallib.php @@ -169,12 +169,13 @@ class assign_submission_onlinetext extends assign_submission_plugin { * @return array */ private function get_edit_options() { - $editoroptions = array( - 'noclean' => false, - 'maxfiles' => EDITOR_UNLIMITED_FILES, - 'maxbytes' => $this->assignment->get_course()->maxbytes, - 'context' => $this->assignment->get_context(), - 'return_types' => (FILE_INTERNAL | FILE_EXTERNAL | FILE_CONTROLLED_LINK) + $editoroptions = array( + 'noclean' => false, + 'maxfiles' => EDITOR_UNLIMITED_FILES, + 'maxbytes' => $this->assignment->get_course()->maxbytes, + 'context' => $this->assignment->get_context(), + 'return_types' => (FILE_INTERNAL | FILE_EXTERNAL | FILE_CONTROLLED_LINK), + 'removeorphaneddrafts' => true // Whether or not to remove any draft files which aren't referenced in the text. ); return $editoroptions; } From 81ed702156743ccd8f666e0639c9d238fd4c4554 Mon Sep 17 00:00:00 2001 From: Jake Dallimore Date: Thu, 17 Aug 2017 16:39:09 +0800 Subject: [PATCH 2/2] MDL-36985 atto: show file removal text on managefiles plugin Shows a text warning in the manage files plugin window when an editor has set the 'removeorphaneddrafts' option to true. The text tells the user that unused files will be automatically deleted on save. --- .../atto/plugins/managefiles/lang/en/atto_managefiles.php | 1 + lib/editor/atto/plugins/managefiles/lib.php | 3 ++- lib/editor/atto/plugins/managefiles/manage.php | 7 ++++--- lib/editor/atto/plugins/managefiles/manage_form.php | 7 +++++++ lib/form/editor.php | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/editor/atto/plugins/managefiles/lang/en/atto_managefiles.php b/lib/editor/atto/plugins/managefiles/lang/en/atto_managefiles.php index 53db50bd918..f98243e0191 100644 --- a/lib/editor/atto/plugins/managefiles/lang/en/atto_managefiles.php +++ b/lib/editor/atto/plugins/managefiles/lang/en/atto_managefiles.php @@ -29,4 +29,5 @@ $string['managefiles'] = 'Manage files'; $string['missingfiles'] = 'Missing files'; $string['pluginname'] = 'Manage files'; $string['unusedfilesdesc'] = 'The following embedded files are not used in the text area:'; +$string['unusedfilesremovalnotice'] = 'Any unused files will be automatically deleted when saving changes.'; $string['unusedfilesheader'] = 'Unused files'; diff --git a/lib/editor/atto/plugins/managefiles/lib.php b/lib/editor/atto/plugins/managefiles/lib.php index bce1bd469fe..b6ff2a1a259 100644 --- a/lib/editor/atto/plugins/managefiles/lib.php +++ b/lib/editor/atto/plugins/managefiles/lib.php @@ -55,7 +55,8 @@ function atto_managefiles_params_for_js($elementid, $options, $fpoptions) { if (!$disabled) { $params['usercontext'] = context_user::instance($USER->id)->id; - foreach (array('itemid', 'context', 'areamaxbytes', 'maxbytes', 'subdirs', 'return_types') as $key) { + foreach (array('itemid', 'context', 'areamaxbytes', 'maxbytes', 'subdirs', 'return_types', + 'removeorphaneddrafts') as $key) { if (isset($options[$key])) { if ($key === 'context' && is_object($options[$key])) { // Just context id is enough. diff --git a/lib/editor/atto/plugins/managefiles/manage.php b/lib/editor/atto/plugins/managefiles/manage.php index ff6922bdf56..5dd39a49481 100644 --- a/lib/editor/atto/plugins/managefiles/manage.php +++ b/lib/editor/atto/plugins/managefiles/manage.php @@ -35,6 +35,7 @@ $return_types = optional_param('return_types', null, PARAM_INT); $areamaxbytes = optional_param('areamaxbytes', FILE_AREA_MAX_BYTES_UNLIMITED, PARAM_INT); $contextid = optional_param('context', SYSCONTEXTID, PARAM_INT); $elementid = optional_param('elementid', '', PARAM_TEXT); +$removeorphaneddrafts = optional_param('removeorphaneddrafts', 0, PARAM_INT); $context = context::instance_by_id($contextid); if ($context->contextlevel == CONTEXT_MODULE) { @@ -75,7 +76,7 @@ $options = array( 'accepted_types' => $accepted_types, 'areamaxbytes' => $areamaxbytes, 'return_types' => $return_types, - 'context' => $context, + 'context' => $context ); $usercontext = context_user::instance($USER->id); @@ -87,8 +88,8 @@ foreach ($files as $file) { } $mform = new atto_managefiles_manage_form(null, - array('options' => $options, 'draftitemid' => $itemid, 'files' => $filenames, 'elementid' => $elementid), - 'post', '', array('id' => 'atto_managefiles_manageform')); + array('options' => $options, 'draftitemid' => $itemid, 'files' => $filenames, 'elementid' => $elementid, + 'removeorphaneddrafts' => $removeorphaneddrafts), 'post', '', array('id' => 'atto_managefiles_manageform')); if ($data = $mform->get_data()) { if (!empty($data->deletefile)) { diff --git a/lib/editor/atto/plugins/managefiles/manage_form.php b/lib/editor/atto/plugins/managefiles/manage_form.php index 50bb8dd5dfa..a5569aa92a5 100644 --- a/lib/editor/atto/plugins/managefiles/manage_form.php +++ b/lib/editor/atto/plugins/managefiles/manage_form.php @@ -47,6 +47,7 @@ class atto_managefiles_manage_form extends moodleform { $elementid = $this->_customdata['elementid']; $options = $this->_customdata['options']; $files = $this->_customdata['files']; + $removeorphaneddrafts = $this->_customdata['removeorphaneddrafts']; $mform->addElement('header', 'filemanagerhdr', get_string('filemanager', 'atto_managefiles')); @@ -69,6 +70,12 @@ class atto_managefiles_manage_form extends moodleform { $mform->addElement('filemanager', 'files_filemanager', '', null, $options); + // Let the user know that any drafts not referenced in the text will be removed automatically. + if ($removeorphaneddrafts) { + $mform->addElement('static', '', '', + html_writer::tag('div', get_string('unusedfilesremovalnotice', 'atto_managefiles'))); + } + $mform->addElement('header', 'missingfileshdr', get_string('missingfiles', 'atto_managefiles')); $mform->addElement('static', '', '', html_writer::tag('div', diff --git a/lib/form/editor.php b/lib/form/editor.php index 69a78127219..d32527ab3e7 100644 --- a/lib/form/editor.php +++ b/lib/form/editor.php @@ -58,7 +58,7 @@ 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' => 15, 'enable_filemanagement' => true); + 'return_types' => 15, 'enable_filemanagement' => true, 'removeorphaneddrafts' => false); // 15 is $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK. /** @var array values for editor */