From 1e9ba157828914d333d64c71b8aa4c24abf62f8d Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sun, 1 Apr 2018 20:32:57 +0800 Subject: [PATCH 1/3] MDL-61819 editor: Implement editor null providers --- .../textarea/classes/privacy/provider.php | 46 +++++++++++++++++++ .../textarea/lang/en/editor_textarea.php | 1 + .../tinymce/classes/privacy/provider.php | 46 +++++++++++++++++++ lib/editor/tinymce/lang/en/editor_tinymce.php | 1 + 4 files changed, 94 insertions(+) create mode 100644 lib/editor/textarea/classes/privacy/provider.php create mode 100644 lib/editor/tinymce/classes/privacy/provider.php diff --git a/lib/editor/textarea/classes/privacy/provider.php b/lib/editor/textarea/classes/privacy/provider.php new file mode 100644 index 00000000000..3a37aabebc5 --- /dev/null +++ b/lib/editor/textarea/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for editor_textarea. + * + * @package editor_textarea + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace editor_textarea\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for editor_textarea implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @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() { + return 'privacy:metadata'; + } +} diff --git a/lib/editor/textarea/lang/en/editor_textarea.php b/lib/editor/textarea/lang/en/editor_textarea.php index a3aa32f9f19..e4df7ef1b63 100644 --- a/lib/editor/textarea/lang/en/editor_textarea.php +++ b/lib/editor/textarea/lang/en/editor_textarea.php @@ -25,3 +25,4 @@ */ $string['pluginname'] = 'Plain text area'; +$string['privacy:metadata'] = 'The editor_textarea plugin does not store any personal data.'; diff --git a/lib/editor/tinymce/classes/privacy/provider.php b/lib/editor/tinymce/classes/privacy/provider.php new file mode 100644 index 00000000000..06db269af67 --- /dev/null +++ b/lib/editor/tinymce/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for editor_tinymce. + * + * @package editor_tinymce + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace editor_tinymce\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for editor_tinymce implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @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() { + return 'privacy:metadata'; + } +} diff --git a/lib/editor/tinymce/lang/en/editor_tinymce.php b/lib/editor/tinymce/lang/en/editor_tinymce.php index 0f0cafbe230..82dd895280a 100644 --- a/lib/editor/tinymce/lang/en/editor_tinymce.php +++ b/lib/editor/tinymce/lang/en/editor_tinymce.php @@ -894,3 +894,4 @@ $string['advanced:cut_desc'] = 'Cut'; $string['advanced:paste_desc'] = 'Paste'; $string['advanced:shortcuts_desc'] = 'Accessibility help'; $string['autosave:restore_content'] = 'Restore auto-saved content'; +$string['privacy:metadata'] = 'The editor_tinymce plugin does not store any personal data.'; From d885504581e61c921abeb0ec5b41d40138bb2229 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sun, 8 Apr 2018 13:27:37 +0800 Subject: [PATCH 2/3] MDL-61819 editor_atto: Implement null provider --- lib/editor/atto/classes/privacy/provider.php | 182 +++++++ lib/editor/atto/lang/en/editor_atto.php | 5 + lib/editor/atto/tests/privacy_provider.php | 470 +++++++++++++++++++ 3 files changed, 657 insertions(+) create mode 100644 lib/editor/atto/classes/privacy/provider.php create mode 100644 lib/editor/atto/tests/privacy_provider.php diff --git a/lib/editor/atto/classes/privacy/provider.php b/lib/editor/atto/classes/privacy/provider.php new file mode 100644 index 00000000000..5f73a5a6c33 --- /dev/null +++ b/lib/editor/atto/classes/privacy/provider.php @@ -0,0 +1,182 @@ +. + +/** + * Privacy Subsystem implementation for editor_atto. + * + * @package editor_atto + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace editor_atto\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\helper; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; + +/** + * Privacy Subsystem implementation for editor_atto. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The Atto editor stores user provided data. + \core_privacy\local\metadata\provider, + + // The Atto editor provides data directly to core. + \core_privacy\local\request\plugin\provider { + + /** + * Returns information about how editor_atto stores its data. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) { + // There isn't much point giving details about the pageid, etc. + $collection->add_database_table('editor_atto_autosave', [ + 'userid' => 'privacy:metadata:database:atto_autosave:userid', + 'drafttext' => 'privacy:metadata:database:atto_autosave:drafttext', + 'timemodified' => 'privacy:metadata:database:atto_autosave:timemodified', + ], 'privacy:metadata:database:atto_autosave'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid($userid) { + // This block doesn't know who information is stored against unless it + // is at the user context. + $contextlist = new \core_privacy\local\request\contextlist(); + $contextuser = \context_user::instance($userid); + + $sql = "SELECT contextid FROM {editor_atto_autosave} WHERE userid = :userid OR contextid = :contextid"; + $params = [ + 'userid' => $userid, + 'contextid' => $contextuser->id, + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['userid'] = $contextlist->get_user()->id; + + $sql = "SELECT * + FROM {editor_atto_autosave} + WHERE + (userid = :userid AND contextid {$contextsql}) + OR + (contextid = :usercontext)"; + + $usercontext = \context_user::instance($user->id); + $contextparams['usercontext'] = $usercontext->id; + $autosaves = $DB->get_recordset_sql($sql, $contextparams); + + foreach ($autosaves as $autosave) { + $context = \context::instance_by_id($autosave->contextid); + $subcontext = [ + get_string('autosaves', 'editor_atto'), + $autosave->id, + ]; + + $html = writer::with_context($context) + ->rewrite_pluginfile_urls($subcontext, 'user', 'draft', $autosave->draftid, $autosave->drafttext); + + $data = (object) [ + 'drafttext' => format_text($html, FORMAT_HTML, static::get_filter_options()), + 'timemodified' => \core_privacy\local\request\transform::datetime($autosave->timemodified), + ]; + + if ($autosave->userid != $user->id) { + $data->author = \core_privacy\local\request\transform::user($autosave->userid); + } + + writer::with_context($context) + ->export_data($subcontext, $data) + ->export_area_files($subcontext, 'user', 'draft', $autosave->draftid); + } + $autosaves->close(); + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + $DB->delete_records('editor_atto_autosave', [ + 'contextid' => $context->id, + ]); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['userid'] = $user->id; + + $sql = "SELECT * FROM {editor_atto_autosave} WHERE contextid {$contextsql}"; + $autosaves = $DB->delete_records_select('editor_atto_autosave', "userid = :userid AND contextid {$contextsql}", + $contextparams); + } + + /** + * Get the filter options. + * + * This is shared to allow unit testing too. + * + * @return \stdClass + */ + public static function get_filter_options() { + return (object) [ + 'overflowdiv' => true, + 'noclean' => true, + ]; + } +} diff --git a/lib/editor/atto/lang/en/editor_atto.php b/lib/editor/atto/lang/en/editor_atto.php index ed7f0878af6..fe62b3aae10 100644 --- a/lib/editor/atto/lang/en/editor_atto.php +++ b/lib/editor/atto/lang/en/editor_atto.php @@ -46,3 +46,8 @@ $string['plugin_title_shortcut'] = '{$a->title} [{$a->shortcut}]'; $string['recover'] = 'Recover'; $string['infostatus'] = 'Information'; $string['warningstatus'] = 'Warning'; +$string['autosaves'] = 'Editor autosave information'; +$string['privacy:metadata:database:atto_autosave'] = 'Editor drafts which was automatically saved.'; +$string['privacy:metadata:database:atto_autosave:userid'] = 'The ID of the user who\'s data was saved.'; +$string['privacy:metadata:database:atto_autosave:drafttext'] = 'The text which was saved.'; +$string['privacy:metadata:database:atto_autosave:timemodified'] = 'The time that content was modified.'; diff --git a/lib/editor/atto/tests/privacy_provider.php b/lib/editor/atto/tests/privacy_provider.php new file mode 100644 index 00000000000..456d7a63546 --- /dev/null +++ b/lib/editor/atto/tests/privacy_provider.php @@ -0,0 +1,470 @@ +. + +/** + * Unit tests for the editor_atto implementation of the privacy API. + * + * @package editor_atto + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \editor_atto\privacy\provider; + +/** + * Unit tests for the editor_atto implementation of the privacy API. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class editor_atto_privacy_testcase extends \core_privacy\tests\provider_testcase { + /** + * One test to check fetch and export of all drafts. + */ + public function test_fetch_and_exports_drafts() { + global $USER; + $this->resetAfterTest(); + + // Create editor drafts in: + // - the system; and + // - a course; and + // - current user context; and + // - another user. + + $systemcontext = \context_system::instance(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $usercontextids = []; + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $usercontext = \context_user::instance($user->id); + $usercontextids[] = $usercontext->id; + $usercontextids[] = $systemcontext->id; + $usercontextids[] = $coursecontext->id; + + // Add a fake inline image to the original post. + + $userdraftintro = $this->create_editor_draft($usercontext, $user->id, + 'id_user_intro', 'text for test user at own context'); + $userdraftdescription = $this->create_editor_draft($usercontext, $user->id, + 'id_user_description', 'text for test user at own context'); + $systemuserdraftintro = $this->create_editor_draft($systemcontext, $user->id, + 'id_system_intro', 'text for test user at system context', 2); + $systemuserdraftdescription = $this->create_editor_draft($systemcontext, $user->id, + 'id_system_description', 'text for test user at system context', 4); + $coursedraftintro = $this->create_editor_draft($coursecontext, $user->id, + 'id_course_intro', 'text for test user at course context'); + $coursedraftdescription = $this->create_editor_draft($coursecontext, $user->id, + 'id_course_description', 'text for test user at course context'); + + // Create some data as the other user too. + $otherusercontextids = []; + $otheruser = $this->getDataGenerator()->create_user(); + $this->setUser($otheruser); + + $otherusercontext = \context_user::instance($otheruser->id); + $otherusercontextids[] = $otherusercontext->id; + $otherusercontextids[] = $systemcontext->id; + $otherusercontextids[] = $coursecontext->id; + + $otheruserdraftintro = $this->create_editor_draft($otherusercontext, $otheruser->id, + 'id_user_intro', 'text for other user at own context'); + $otheruserdraftdescription = $this->create_editor_draft($otherusercontext, $otheruser->id, + 'id_user_description', 'text for other user at own context'); + $systemotheruserdraftintro = $this->create_editor_draft($systemcontext, $otheruser->id, + 'id_system_intro', 'text for other user at system context'); + $systemotheruserdraftdescription = $this->create_editor_draft($systemcontext, $otheruser->id, + 'id_system_description', 'text for other user at system context'); + $courseotheruserdraftintro = $this->create_editor_draft($coursecontext, $otheruser->id, + 'id_course_intro', 'text for other user at course context'); + $courseotheruserdraftdescription = $this->create_editor_draft($coursecontext, $otheruser->id, + 'id_course_description', 'text for other user at course context'); + + // Test as the original user. + // Get all context data for the original user. + $this->setUser($user); + $contextlist = provider::get_contexts_for_userid($user->id); + + // There are three contexts in the list. + $this->assertCount(3, $contextlist); + + // Check the list against the expected list of contexts. + foreach ($contextlist as $context) { + $this->assertContains($context->id, $usercontextids); + } + + // Export the data for the system context. + // There should be two. + $this->export_context_data_for_user($user->id, $systemcontext, 'editor_atto'); + $writer = \core_privacy\local\request\writer::with_context($systemcontext); + $this->assertTrue($writer->has_any_data()); + + $subcontextbase = [get_string('autosaves', 'editor_atto')]; + + // There should be an intro and description. + $intro = $writer->get_data(array_merge($subcontextbase, [$systemuserdraftintro->id])); + $fs = get_file_storage(); + $this->assertEquals( + format_text($systemuserdraftintro->drafttext, FORMAT_HTML, provider::get_filter_options()), + $intro->drafttext + ); + $this->assertCount(2, $writer->get_files(array_merge($subcontextbase, [$systemuserdraftintro->id]))); + + $description = $writer->get_data(array_merge($subcontextbase, [$systemuserdraftdescription->id])); + $this->assertEquals( + format_text($systemuserdraftdescription->drafttext, FORMAT_HTML, provider::get_filter_options()), + $description->drafttext + ); + $this->assertCount(4, $writer->get_files(array_merge($subcontextbase, [$systemuserdraftdescription->id]))); + } + + /** + * Test delete_for_all_users_in_context. + */ + public function test_delete_for_all_users_in_context() { + global $USER, $DB; + $this->resetAfterTest(); + + // Create editor drafts in: + // - the system; and + // - a course; and + // - current user context; and + // - another user. + + $systemcontext = \context_system::instance(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $usercontextids = []; + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $usercontext = \context_user::instance($user->id); + $usercontextids[] = $usercontext->id; + $usercontextids[] = $systemcontext->id; + $usercontextids[] = $coursecontext->id; + + // Add a fake inline image to the original post. + + $userdraftintro = $this->create_editor_draft($usercontext, $user->id, + 'id_user_intro', 'text for test user at own context'); + $userdraftdescription = $this->create_editor_draft($usercontext, $user->id, + 'id_user_description', 'text for test user at own context'); + $systemuserdraftintro = $this->create_editor_draft($systemcontext, $user->id, + 'id_system_intro', 'text for test user at system context', 2); + $systemuserdraftdescription = $this->create_editor_draft($systemcontext, $user->id, + 'id_system_description', 'text for test user at system context', 4); + $coursedraftintro = $this->create_editor_draft($coursecontext, $user->id, + 'id_course_intro', 'text for test user at course context'); + $coursedraftdescription = $this->create_editor_draft($coursecontext, $user->id, + 'id_course_description', 'text for test user at course context'); + + // Create some data as the other user too. + $otherusercontextids = []; + $otheruser = $this->getDataGenerator()->create_user(); + $this->setUser($otheruser); + + $otherusercontext = \context_user::instance($otheruser->id); + $otherusercontextids[] = $otherusercontext->id; + $otherusercontextids[] = $systemcontext->id; + $otherusercontextids[] = $coursecontext->id; + + $otheruserdraftintro = $this->create_editor_draft($otherusercontext, $otheruser->id, + 'id_user_intro', 'text for other user at own context'); + $otheruserdraftdescription = $this->create_editor_draft($otherusercontext, $otheruser->id, + 'id_user_description', 'text for other user at own context'); + $systemotheruserdraftintro = $this->create_editor_draft($systemcontext, $otheruser->id, + 'id_system_intro', 'text for other user at system context'); + $systemotheruserdraftdescription = $this->create_editor_draft($systemcontext, $otheruser->id, + 'id_system_description', 'text for other user at system context'); + $courseotheruserdraftintro = $this->create_editor_draft($coursecontext, $otheruser->id, + 'id_course_intro', 'text for other user at course context'); + $courseotheruserdraftdescription = $this->create_editor_draft($coursecontext, $otheruser->id, + 'id_course_description', 'text for other user at course context'); + + // Test deletion of the user context. + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $usercontext->id])); + provider::delete_data_for_all_users_in_context($usercontext); + $this->assertCount(0, $DB->get_records('editor_atto_autosave', ['contextid' => $usercontext->id])); + + // No other contexts should be removed. + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $otherusercontext->id])); + $this->assertCount(4, $DB->get_records('editor_atto_autosave', ['contextid' => $systemcontext->id])); + $this->assertCount(4, $DB->get_records('editor_atto_autosave', ['contextid' => $coursecontext->id])); + + // Test deletion of the course contexts. + provider::delete_data_for_all_users_in_context($coursecontext); + $this->assertCount(0, $DB->get_records('editor_atto_autosave', ['contextid' => $coursecontext->id])); + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $otherusercontext->id])); + $this->assertCount(4, $DB->get_records('editor_atto_autosave', ['contextid' => $systemcontext->id])); + + // Test deletion of the system contexts. + provider::delete_data_for_all_users_in_context($systemcontext); + $this->assertCount(0, $DB->get_records('editor_atto_autosave', ['contextid' => $systemcontext->id])); + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $otherusercontext->id])); + } + + /** + * Test delete_for_all_users_in_context. + */ + public function test_delete_for_user_in_contexts() { + global $USER, $DB; + $this->resetAfterTest(); + + // Create editor drafts in: + // - the system; and + // - a course; and + // - current user context; and + // - another user. + + $systemcontext = \context_system::instance(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + + $usercontextids = []; + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $usercontext = \context_user::instance($user->id); + $usercontextids[] = $usercontext->id; + $usercontextids[] = $systemcontext->id; + $usercontextids[] = $coursecontext->id; + + // Add a fake inline image to the original post. + + $userdraftintro = $this->create_editor_draft($usercontext, $user->id, + 'id_user_intro', 'text for test user at own context'); + $userdraftdescription = $this->create_editor_draft($usercontext, $user->id, + 'id_user_description', 'text for test user at own context'); + $systemuserdraftintro = $this->create_editor_draft($systemcontext, $user->id, + 'id_system_intro', 'text for test user at system context', 2); + $systemuserdraftdescription = $this->create_editor_draft($systemcontext, $user->id, + 'id_system_description', 'text for test user at system context', 4); + $coursedraftintro = $this->create_editor_draft($coursecontext, $user->id, + 'id_course_intro', 'text for test user at course context'); + $coursedraftdescription = $this->create_editor_draft($coursecontext, $user->id, + 'id_course_description', 'text for test user at course context'); + + // Create some data as the other user too. + $otherusercontextids = []; + $otheruser = $this->getDataGenerator()->create_user(); + $this->setUser($otheruser); + + $otherusercontext = \context_user::instance($otheruser->id); + $otherusercontextids[] = $otherusercontext->id; + $otherusercontextids[] = $systemcontext->id; + $otherusercontextids[] = $coursecontext->id; + + $otheruserdraftintro = $this->create_editor_draft($otherusercontext, $otheruser->id, + 'id_user_intro', 'text for other user at own context'); + $otheruserdraftdescription = $this->create_editor_draft($otherusercontext, $otheruser->id, + 'id_user_description', 'text for other user at own context'); + $systemotheruserdraftintro = $this->create_editor_draft($systemcontext, $otheruser->id, + 'id_system_intro', 'text for other user at system context'); + $systemotheruserdraftdescription = $this->create_editor_draft($systemcontext, $otheruser->id, + 'id_system_description', 'text for other user at system context'); + $courseotheruserdraftintro = $this->create_editor_draft($coursecontext, $otheruser->id, + 'id_course_intro', 'text for other user at course context'); + $courseotheruserdraftdescription = $this->create_editor_draft($coursecontext, $otheruser->id, + 'id_course_description', 'text for other user at course context'); + + // Test deletion of all data for user in usercontext only. + $contextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($user->id), + 'editor_atto', + [$usercontext->id] + ); + provider::delete_data_for_user($contextlist); + $this->assertCount(0, $DB->get_records('editor_atto_autosave', ['contextid' => $usercontext->id])); + + // No other contexts should be removed. + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $otherusercontext->id])); + $this->assertCount(4, $DB->get_records('editor_atto_autosave', ['contextid' => $systemcontext->id])); + $this->assertCount(4, $DB->get_records('editor_atto_autosave', ['contextid' => $coursecontext->id])); + + // Test deletion of all data for user in course and system. + $contextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($user->id), + 'editor_atto', + [$coursecontext->id, $systemcontext->id] + ); + provider::delete_data_for_user($contextlist); + $this->assertCount(0, $DB->get_records('editor_atto_autosave', ['contextid' => $usercontext->id])); + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $otherusercontext->id])); + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $systemcontext->id])); + $this->assertCount(2, $DB->get_records('editor_atto_autosave', ['contextid' => $coursecontext->id])); + + // Data for the other user should remain. + $this->assertCount(2, $DB->get_records('editor_atto_autosave', [ + 'contextid' => $coursecontext->id, + 'userid' => $otheruser->id, + ])); + + $this->assertCount(2, $DB->get_records('editor_atto_autosave', [ + 'contextid' => $systemcontext->id, + 'userid' => $otheruser->id, + ])); + } + + /** + * Test fetch and delete when another user has editted a draft in your + * user context. Edge case. + */ + public function test_another_user_edits_you() { + global $USER, $DB; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + $otheruser = $this->getDataGenerator()->create_user(); + $otherusercontext = \context_user::instance($otheruser->id); + $this->setUser($user); + + $userdraftintro = $this->create_editor_draft($usercontext, $otheruser->id, + 'id_user_intro', 'text for test user at other context'); + + // Test as the owning user. + $this->setUser($user); + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + $firstcontext = reset($contexts); + $this->assertEquals($usercontext, $firstcontext); + + // Should have the data. + $this->export_context_data_for_user($user->id, $usercontext, 'editor_atto'); + $writer = \core_privacy\local\request\writer::with_context($usercontext); + $this->assertTrue($writer->has_any_data()); + + $subcontext = [ + get_string('autosaves', 'editor_atto'), + $userdraftintro->id, + ]; + $data = $writer->get_data($subcontext); + $this->assertEquals(\core_privacy\local\request\transform::user($otheruser->id), $data->author); + + $contextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($user->id), + 'editor_atto', + [$usercontext->id] + ); + + + // Deleting for this context should _not_ delete as the user does not own this draft (crazy edge case, remember). + provider::delete_data_for_user($contextlist); + $records = $DB->get_records('editor_atto_autosave'); + $this->assertNotEmpty($records); + $this->assertCount(1, $records); + $firstrecord = reset($records); + $this->assertEquals($userdraftintro->id, $firstrecord->id); + } + + /** + * Test fetch and delete when you have edited another user's context. + */ + public function test_another_you_edit_different_user() { + global $USER, $DB; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + $otheruser = $this->getDataGenerator()->create_user(); + $otherusercontext = \context_user::instance($otheruser->id); + $this->setUser($user); + + $userdraftintro = $this->create_editor_draft($otherusercontext, $user->id, + 'id_user_intro', 'text for other user you just edited.'); + + // Test as the context owner. + $this->setUser($user); + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + $firstcontext = reset($contexts); + $this->assertEquals($otherusercontext, $firstcontext); + + // Should have the data. + $this->export_context_data_for_user($user->id, $otherusercontext, 'editor_atto'); + $writer = \core_privacy\local\request\writer::with_context($otherusercontext); + $this->assertTrue($writer->has_any_data()); + + $subcontext = [ + get_string('autosaves', 'editor_atto'), + $userdraftintro->id, + ]; + $data = $writer->get_data($subcontext); + $this->assertFalse(isset($data->author)); + + $contextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($user->id), + 'editor_atto', + [$otherusercontext->id] + ); + provider::delete_data_for_user($contextlist); + $this->assertEmpty($DB->get_records('editor_atto_autosave')); + } + + /** + * Create an editor draft. + * + * @param \context $context The context to create the draft for. + * @param int $userid The ID to create the draft for. + * @param string $elementid The elementid for the editor. + * @param string $text The text to write. + * @param int $filecount The number of files to create. + * @return \stdClass The editor draft. + */ + protected function create_editor_draft(\context $context, $userid, $elementid, $text, $filecount = 0) { + global $DB; + + $draftid = file_get_unused_draft_itemid(); + $fs = get_file_storage(); + + for ($i = 0; $i < $filecount; $i++) { + $fs->create_file_from_string([ + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftid, + 'filepath' => '/', + 'filename' => "example_{$i}.txt", + ], + "Awesome example of a text file with id {$i} for {$context->id} and {$elementid}"); + } + + $id = $DB->insert_record('editor_atto_autosave', (object) [ + 'elementid' => $elementid, + 'contextid' => $context->id, + 'userid' => $userid, + 'drafttext' => $text, + 'draftid' => $draftid, + 'pageinstance' => 'example_page_instance_' . rand(1, 1000), + 'timemodified' => time(), + + // Page hash doesn't matter for our purposes. + 'pagehash' => sha1("{$userid}/{$context->id}/{$elementid}/{$draftid}"), + ]); + + return $DB->get_record('editor_atto_autosave', ['id' => $id]); + } +} From 86feef9a0127066675241347658662ff3aade331 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sun, 8 Apr 2018 18:40:20 +0800 Subject: [PATCH 3/3] MDL-61819 core_editor: Implement core provider --- lang/en/editor.php | 2 + lib/editor/classes/privacy/provider.php | 71 +++++++++++++++++++ lib/editor/tests/privacy_provider_test.php | 79 ++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 lib/editor/classes/privacy/provider.php create mode 100644 lib/editor/tests/privacy_provider_test.php diff --git a/lang/en/editor.php b/lang/en/editor.php index 123bdad3116..54c35b5c876 100644 --- a/lang/en/editor.php +++ b/lang/en/editor.php @@ -117,6 +117,8 @@ $string['pleaseenteralt'] = 'Please enter the alternate text'; $string['popupeditor'] = 'Enlarge Editor'; $string['preformatted'] = 'Preformatted'; $string['preview'] = 'Preview'; +$string['privacy:metadata:preference:htmleditor'] = 'The preferred editor to use when using an HTML Text Area'; +$string['privacy:preference:htmleditor'] = 'Your preferred editor to use for writing HTML text is {$a}'; $string['properties'] = 'Properties'; $string['redo'] = 'Redo your last action'; $string['regularexpressions'] = 'Use regular expressions'; diff --git a/lib/editor/classes/privacy/provider.php b/lib/editor/classes/privacy/provider.php new file mode 100644 index 00000000000..24d0aa85362 --- /dev/null +++ b/lib/editor/classes/privacy/provider.php @@ -0,0 +1,71 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package core_editor + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_editor\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; + +/** + * Provider for the editor API. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The Editor subsystem does not store any data itself. + // It has no database tables, and it purely acts as a conduit to the various editors. + \core_privacy\local\metadata\provider, + + // The Editor subsystem has user preferences. + \core_privacy\local\request\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) { + $collection->add_user_preference('htmleditor', 'privacy:metadata:preference:htmleditor'); + + return $collection; + } + + /** + * Export all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences($userid) { + $preference = get_user_preferences('htmleditor'); + if (null !== $preference) { + $desc = get_string('privacy:preference:htmleditor', 'core_editor', + get_string('pluginname', "editor_{$preference}")); + writer::export_user_preference('core_editor', 'htmleditor', $preference, $desc); + } + } +} diff --git a/lib/editor/tests/privacy_provider_test.php b/lib/editor/tests/privacy_provider_test.php new file mode 100644 index 00000000000..d33e0071d17 --- /dev/null +++ b/lib/editor/tests/privacy_provider_test.php @@ -0,0 +1,79 @@ +. + +/** + * Privacy provider tests. + * + * @package core_editor + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; +use core_editor\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy provider tests class. + * + * @package core_editor + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_editor_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + /** + * When no preference exists, there should be no export. + */ + public function test_no_preference() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + provider::export_user_preferences($USER->id); + $this->assertFalse(writer::with_context(\context_system::instance())->has_any_data()); + } + + /** + * When an editor is set, the name of that editor will be reported. + */ + public function test_editor_atto() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('htmleditor', 'atto'); + + provider::export_user_preferences($USER->id); + $this->assertTrue(writer::with_context(\context_system::instance())->has_any_data()); + + $prefs = writer::with_context(\context_system::instance())->get_user_preferences('core_editor'); + $this->assertNotEmpty($prefs->htmleditor); + $this->assertNotEmpty($prefs->htmleditor->value); + $this->assertNotEmpty($prefs->htmleditor->description); + $this->assertEquals('atto', $prefs->htmleditor->value); + + $this->assertEquals( + get_string( + 'privacy:preference:htmleditor', + 'core_editor', + get_string('pluginname', "editor_atto") + ), + $prefs->htmleditor->description + ); + } +}