From 2b3c4538f1661673e221ac706b0f901a13c04567 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Mon, 16 Apr 2018 13:43:23 +0800 Subject: [PATCH 1/6] MDL-61920 ltiservice_gradebookservices: implement privacy provider --- .../classes/privacy/provider.php | 94 +++++++++++++++++++ .../lang/en/ltiservice_gradebookservices.php | 6 ++ .../tests/privacy_provider_test.php | 51 ++++++++++ 3 files changed, 151 insertions(+) create mode 100644 mod/lti/service/gradebookservices/classes/privacy/provider.php create mode 100644 mod/lti/service/gradebookservices/tests/privacy_provider_test.php diff --git a/mod/lti/service/gradebookservices/classes/privacy/provider.php b/mod/lti/service/gradebookservices/classes/privacy/provider.php new file mode 100644 index 00000000000..878fe6bd991 --- /dev/null +++ b/mod/lti/service/gradebookservices/classes/privacy/provider.php @@ -0,0 +1,94 @@ +. + +/** + * Privacy Subsystem implementation for ltiservice_gradebookservices. + * + * @package ltiservice_gradebookservices + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_gradebookservices\privacy; + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for ltiservice_gradebookservices. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\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 { + $collection->link_external_location('External LTI provider.', [ + 'userid' => 'privacy:metadata:userid', + 'grade' => 'privacy:metadata:grade', + 'maxgrade' => 'privacy:metadata:maxgrade', + 'feedback' => 'privacy:metadata:feedback', + 'timemodified' => 'privacy:metadata:timemodified' + ], 'privacy:metadata:externalpurpose'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + return new 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) { + } + + /** + * Delete all user data which matches the specified context. + * + * @param \context $context A user context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + } + + /** + * 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) { + } +} diff --git a/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php b/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php index e68fdc5366f..7d9a5d149d0 100644 --- a/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php +++ b/mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php @@ -34,5 +34,11 @@ $string['modulename'] = 'LTI Grades'; $string['nevergs'] = 'Do not use this service'; $string['partialgs'] = 'Use this service for grade sync only'; $string['pluginname'] = 'LTI Assignment and Grade Services'; +$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.'; +$string['privacy:metadata:feedback'] = 'The feedback the user received for this LTI activity.'; +$string['privacy:metadata:grade'] = 'The grade the user received in Moodle for this LTI activity.'; +$string['privacy:metadata:maxgrade'] = 'The max grade that can be achieved for this LTI activity.'; +$string['privacy:metadata:timemodified'] = 'The last time the grade was updated'; +$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.'; $string['servicename'] = 'LTI Assignment and Grade Services'; $string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup'; diff --git a/mod/lti/service/gradebookservices/tests/privacy_provider_test.php b/mod/lti/service/gradebookservices/tests/privacy_provider_test.php new file mode 100644 index 00000000000..d24c59c8f12 --- /dev/null +++ b/mod/lti/service/gradebookservices/tests/privacy_provider_test.php @@ -0,0 +1,51 @@ +. +/** + * Unit tests for ltiservice_gradebookservices privacy provider. + * + * @package ltiservice_gradebookservices + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\tests\provider_testcase; + +/** + * Unit tests for ltiservice_gradebookservices privacy provider. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ltiservice_gradebookservices_privacy_provider_testcase extends provider_testcase { + + /** + * Basic setup for these tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Test getting the context for the user ID related to this plugin. + */ + public function test_get_contexts_for_userid() { + $user = $this->getDataGenerator()->create_user(); + $contextlist = \ltiservice_gradebookservices\privacy\provider::get_contexts_for_userid($user->id); + $this->assertEmpty($contextlist); + } +} From 03d6e06400756f96eaab089cec9a1ee2317d5383 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Mon, 16 Apr 2018 14:04:44 +0800 Subject: [PATCH 2/6] MDL-61920 ltiservice_memberships: implement privacy provider --- .../memberships/classes/privacy/provider.php | 95 +++++++++++++++++++ .../lang/en/ltiservice_memberships.php | 7 ++ .../tests/privacy_provider_test.php | 51 ++++++++++ 3 files changed, 153 insertions(+) create mode 100644 mod/lti/service/memberships/classes/privacy/provider.php create mode 100644 mod/lti/service/memberships/tests/privacy_provider_test.php diff --git a/mod/lti/service/memberships/classes/privacy/provider.php b/mod/lti/service/memberships/classes/privacy/provider.php new file mode 100644 index 00000000000..83666d0039f --- /dev/null +++ b/mod/lti/service/memberships/classes/privacy/provider.php @@ -0,0 +1,95 @@ +. + +/** + * Privacy Subsystem implementation for ltiservice_memberships. + * + * @package ltiservice_memberships + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_memberships\privacy; + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for ltiservice_memberships. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\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 { + $collection->link_external_location('External LTI provider.', [ + 'userid' => 'privacy:metadata:userid', + 'useridnumber' => 'privacy:metadata:useridnumber', + 'fullname' => 'privacy:metadata:fullname', + 'firstname' => 'privacy:metadata:firstname', + 'lastname' => 'privacy:metadata:lastname', + 'email' => 'privacy:metadata:email' + ], 'privacy:metadata:externalpurpose'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + return new 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) { + } + + /** + * Delete all user data which matches the specified context. + * + * @param \context $context A user context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + } + + /** + * 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) { + } +} diff --git a/mod/lti/service/memberships/lang/en/ltiservice_memberships.php b/mod/lti/service/memberships/lang/en/ltiservice_memberships.php index cd8d220dbe6..8798cabd64c 100644 --- a/mod/lti/service/memberships/lang/en/ltiservice_memberships.php +++ b/mod/lti/service/memberships/lang/en/ltiservice_memberships.php @@ -28,4 +28,11 @@ $string['ltiservice_memberships'] = 'IMS LTI Membership: '; $string['ltiservice_memberships_help'] = 'Allow the tool to retrieve member\'s info from the course using the IMS LTI Membership Service. The privacy settings will apply.'; $string['notallow'] = 'Do not use this service'; $string['pluginname'] = 'Memberships LTI Service'; +$string['privacy:metadata:email'] = 'The email of the user using the LTI consumer.'; +$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.'; +$string['privacy:metadata:firstname'] = 'The firstname of the user using the LTI consumer.'; +$string['privacy:metadata:fullname'] = 'The fullname of the user using the LTI consumer.'; +$string['privacy:metadata:lastname'] = 'The lastname of the user using the LTI consumer.'; +$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.'; +$string['privacy:metadata:useridnumber'] = 'The idnumber of the user using the LTI consumer.'; $string['servicename'] = 'Memberships'; diff --git a/mod/lti/service/memberships/tests/privacy_provider_test.php b/mod/lti/service/memberships/tests/privacy_provider_test.php new file mode 100644 index 00000000000..a2e8d87e623 --- /dev/null +++ b/mod/lti/service/memberships/tests/privacy_provider_test.php @@ -0,0 +1,51 @@ +. +/** + * Unit tests for ltiservice_memberships privacy provider. + * + * @package ltiservice_memberships + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\tests\provider_testcase; + +/** + * Unit tests for ltiservice_memberships privacy provider. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ltiservice_memberships_privacy_provider_testcase extends provider_testcase { + + /** + * Basic setup for these tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Test getting the context for the user ID related to this plugin. + */ + public function test_get_contexts_for_userid() { + $user = $this->getDataGenerator()->create_user(); + $contextlist = \ltiservice_memberships\privacy\provider::get_contexts_for_userid($user->id); + $this->assertEmpty($contextlist); + } +} From 583f84d7cb8e694de50ccd5da5678083fa73042f Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Mon, 16 Apr 2018 14:05:51 +0800 Subject: [PATCH 3/6] MDL-61920 ltiservice_profile: implement privacy provider --- .../profile/classes/privacy/provider.php | 46 +++++++++++++++++++ .../profile/lang/en/ltiservice_profile.php | 1 + 2 files changed, 47 insertions(+) create mode 100644 mod/lti/service/profile/classes/privacy/provider.php diff --git a/mod/lti/service/profile/classes/privacy/provider.php b/mod/lti/service/profile/classes/privacy/provider.php new file mode 100644 index 00000000000..f3407b17ebc --- /dev/null +++ b/mod/lti/service/profile/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for ltiservice_profile. + * + * @package ltiservice_profile + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_profile\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for ltiservice_profile implementing null_provider. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/lti/service/profile/lang/en/ltiservice_profile.php b/mod/lti/service/profile/lang/en/ltiservice_profile.php index 9001c1cecaa..f7ab58777ee 100644 --- a/mod/lti/service/profile/lang/en/ltiservice_profile.php +++ b/mod/lti/service/profile/lang/en/ltiservice_profile.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Tool Consumer Profile LTI Service'; +$string['privacy:metadata'] = 'The Tool Consumer Profile LTI Service plugin does not store any personal data.'; From 897a6e824f7a4f31740c83978e0beac6c11c21c1 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Mon, 16 Apr 2018 14:06:59 +0800 Subject: [PATCH 4/6] MDL-61920 ltiservice_toolproxy: implement privacy provider --- .../toolproxy/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/ltiservice_toolproxy.php | 1 + 2 files changed, 47 insertions(+) create mode 100644 mod/lti/service/toolproxy/classes/privacy/provider.php diff --git a/mod/lti/service/toolproxy/classes/privacy/provider.php b/mod/lti/service/toolproxy/classes/privacy/provider.php new file mode 100644 index 00000000000..e73edf00bbb --- /dev/null +++ b/mod/lti/service/toolproxy/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for ltiservice_toolproxy. + * + * @package ltiservice_toolproxy + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_toolproxy\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for ltiservice_toolproxy implementing null_provider. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/lti/service/toolproxy/lang/en/ltiservice_toolproxy.php b/mod/lti/service/toolproxy/lang/en/ltiservice_toolproxy.php index 4e61740c4b1..5b389a4ddad 100644 --- a/mod/lti/service/toolproxy/lang/en/ltiservice_toolproxy.php +++ b/mod/lti/service/toolproxy/lang/en/ltiservice_toolproxy.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Tool Proxy Service'; +$string['privacy:metadata'] = 'The Tool Proxy Service plugin does not store any personal data.'; From 13fadc876748a56de0ca0e534aa885f34a39cf4e Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Mon, 16 Apr 2018 14:08:24 +0800 Subject: [PATCH 5/6] MDL-61920 ltiservice_toolsettings: implement privacy provider --- .../toolsettings/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/ltiservice_toolsettings.php | 1 + 2 files changed, 47 insertions(+) create mode 100644 mod/lti/service/toolsettings/classes/privacy/provider.php diff --git a/mod/lti/service/toolsettings/classes/privacy/provider.php b/mod/lti/service/toolsettings/classes/privacy/provider.php new file mode 100644 index 00000000000..2161181a486 --- /dev/null +++ b/mod/lti/service/toolsettings/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for ltiservice_toolsettings. + * + * @package ltiservice_toolsettings + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace ltiservice_toolsettings\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for ltiservice_toolsettings implementing null_provider. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php b/mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php index b4043a7e550..59f4bbe5453 100644 --- a/mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php +++ b/mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Tool Settings Service'; +$string['privacy:metadata'] = 'The Tool Settings Service plugin does not store any personal data.'; From 3467e8ce3d18ee31a083c68a78c3c014f534cc08 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Tue, 17 Apr 2018 15:27:24 +0800 Subject: [PATCH 6/6] MDL-61920 mod_lti: implement privacy provider --- mod/lti/classes/privacy/provider.php | 369 ++++++++++++++++++++++++ mod/lti/lang/en/lti.php | 13 + mod/lti/tests/privacy_provider_test.php | 313 ++++++++++++++++++++ 3 files changed, 695 insertions(+) create mode 100644 mod/lti/classes/privacy/provider.php create mode 100644 mod/lti/tests/privacy_provider_test.php diff --git a/mod/lti/classes/privacy/provider.php b/mod/lti/classes/privacy/provider.php new file mode 100644 index 00000000000..1afabf68018 --- /dev/null +++ b/mod/lti/classes/privacy/provider.php @@ -0,0 +1,369 @@ +. + +/** + * Privacy Subsystem implementation for mod_lti. + * + * @package mod_lti + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_lti\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementation for mod_lti. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider { + + /** + * Return the fields which contain personal data. + * + * @param collection $items a reference to the collection to use to store the metadata. + * @return collection the updated collection of metadata items. + */ + public static function get_metadata(collection $items) : collection { + $items->add_database_table( + 'lti_submission', + [ + 'userid' => 'privacy:metadata:lti_submission:userid', + 'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted', + 'dateupdated' => 'privacy:metadata:lti_submission:dateupdated', + 'gradepercent' => 'privacy:metadata:lti_submission:gradepercent', + 'originalgrade' => 'privacy:metadata:lti_submission:originalgrade', + ], + 'privacy:metadata:lti_submission' + ); + + $items->add_database_table( + 'lti_tool_proxies', + [ + 'name' => 'privacy:metadata:lti_tool_proxies:name', + 'createdby' => 'privacy:metadata:createdby', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'privacy:metadata:timemodified' + ], + 'privacy:metadata:lti_tool_proxies' + ); + + $items->add_database_table( + 'lti_types', + [ + 'name' => 'privacy:metadata:lti_types:name', + 'createdby' => 'privacy:metadata:createdby', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'privacy:metadata:timemodified' + ], + 'privacy:metadata:lti_types' + ); + + return $items; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid the userid. + * @return contextlist the list of contexts containing user info for the user. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + // Fetch all LTI submissions. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm + ON cm.id = c.instanceid + AND c.contextlevel = :contextlevel + INNER JOIN {modules} m + ON m.id = cm.module + AND m.name = :modname + INNER JOIN {lti} lti + ON lti.id = cm.instance + INNER JOIN {lti_submission} ltisub + ON ltisub.ltiid = lti.id + WHERE ltisub.userid = :userid"; + + $params = [ + 'modname' => 'lti', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + ]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + + // Fetch all LTI types. + $sql = "SELECT c.id + FROM {context} c + JOIN {course} course + ON c.contextlevel = :contextlevel + AND c.instanceid = course.id + JOIN {lti_types} ltit + ON ltit.course = course.id + WHERE ltit.createdby = :userid"; + + $params = [ + 'contextlevel' => CONTEXT_COURSE, + 'userid' => $userid + ]; + $contextlist->add_from_sql($sql, $params); + + // The LTI tool proxies sit in the system context. + $contextlist->add_system_context(); + + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + self::export_user_data_lti_submissions($contextlist); + + self::export_user_data_lti_types($contextlist); + + self::export_user_data_lti_tool_proxies($contextlist); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if (!$context instanceof \context_module) { + return; + } + + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('lti_submission', ['ltiid' => $instanceid]); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + if (!$context instanceof \context_module) { + return; + } + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]); + } + } + + /** + * Export personal data for the given approved_contextlist related to LTI submissions. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) { + global $DB; + + // Filter out any contexts that are not related to modules. + $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + + if (empty($cmids)) { + return; + } + + $user = $contextlist->get_user(); + + // Get all the LTI activities associated with the above course modules. + $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids); + $ltiids = array_keys($ltiidstocmids); + + list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['userid' => $user->id]); + $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id'); + self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) { + $carry[] = [ + 'gradepercent' => $record->gradepercent, + 'originalgrade' => $record->originalgrade, + 'datesubmitted' => transform::datetime($record->datesubmitted), + 'dateupdated' => transform::datetime($record->dateupdated) + ]; + return $carry; + }, function($ltiid, $data) use ($user, $ltiidstocmids) { + $context = \context_module::instance($ltiidstocmids[$ltiid]); + $contextdata = helper::get_context_data($context, $user); + $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $finaldata); + }); + } + + /** + * Export personal data for the given approved_contextlist related to LTI types. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + protected static function export_user_data_lti_types(approved_contextlist $contextlist) { + global $DB; + + // Filter out any contexts that are not related to courses. + $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_COURSE) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + + if (empty($courseids)) { + return; + } + + $user = $contextlist->get_user(); + + list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['userid' => $user->id]); + $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC'); + self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) { + $context = \context_course::instance($record->course); + $options = ['context' => $context]; + $carry[] = [ + 'name' => format_string($record->name, true, $options), + 'createdby' => transform::user($record->createdby), + 'timecreated' => transform::datetime($record->timecreated), + 'timemodified' => transform::datetime($record->timemodified) + ]; + return $carry; + }, function($courseid, $data) { + $context = \context_course::instance($courseid); + $finaldata = (object) ['lti_types' => $data]; + writer::with_context($context)->export_data([], $finaldata); + }); + } + + /** + * Export personal data for the given approved_contextlist related to LTI tool proxies. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) { + global $DB; + + // Filter out any contexts that are not related to system context. + $systemcontexts = array_filter($contextlist->get_contexts(), function($context) { + return $context->contextlevel == CONTEXT_SYSTEM; + }); + + if (empty($systemcontexts)) { + return; + } + + $user = $contextlist->get_user(); + + $systemcontext = \context_system::instance(); + + $data = []; + $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC'); + foreach ($ltiproxies as $ltiproxy) { + $data[] = [ + 'name' => format_string($ltiproxy->name, true, $systemcontext), + 'createdby' => transform::user($ltiproxy->createdby), + 'timecreated' => transform::datetime($ltiproxy->timecreated), + 'timemodified' => transform::datetime($ltiproxy->timemodified) + ]; + } + $ltiproxies->close(); + + $finaldata = (object) ['lti_tool_proxies' => $data]; + writer::with_context($systemcontext)->export_data([], $finaldata); + } + + /** + * Return a dict of LTI IDs mapped to their course module ID. + * + * @param array $cmids The course module IDs. + * @return array In the form of [$ltiid => $cmid]. + */ + protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + $sql = "SELECT lti.id, cm.id AS cmid + FROM {lti} lti + JOIN {modules} m + ON m.name = :lti + JOIN {course_modules} cm + ON cm.instance = lti.id + AND cm.module = m.id + WHERE cm.id $insql"; + $params = array_merge($inparams, ['lti' => 'lti']); + + return $DB->get_records_sql_menu($sql, $params); + } + + /** + * Loop and export from a recordset. + * + * @param \moodle_recordset $recordset The recordset. + * @param string $splitkey The record key to determine when to export. + * @param mixed $initial The initial data to reduce from. + * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. + * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. + * @return void + */ + protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, + callable $reducer, callable $export) { + $data = $initial; + $lastid = null; + + foreach ($recordset as $record) { + if ($lastid && $record->{$splitkey} != $lastid) { + $export($lastid, $data); + $data = $initial; + } + $data = $reducer($data, $record); + $lastid = $record->{$splitkey}; + } + $recordset->close(); + + if (!empty($lastid)) { + $export($lastid, $data); + } + } +} diff --git a/mod/lti/lang/en/lti.php b/mod/lti/lang/en/lti.php index 12e68000f18..b567b3aeba6 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -356,6 +356,19 @@ $string['preferwidget'] = 'Prefer widget launch'; $string['preferwidth'] = 'Preferred width'; $string['press_to_submit'] = 'Press to launch this activity'; $string['privacy'] = 'Privacy'; +$string['privacy:metadata:createdby'] = 'The user who created the record'; +$string['privacy:metadata:lti_submission'] = 'LTI submission'; +$string['privacy:metadata:lti_submission:datesubmitted'] = 'The timestamp indicating when the submission was made'; +$string['privacy:metadata:lti_submission:dateupdated'] = 'The timestamp indicating when the submission was modified'; +$string['privacy:metadata:lti_submission:gradepercent'] = 'The grade for the user as a percentage'; +$string['privacy:metadata:lti_submission:originalgrade'] = 'The original grade for the user'; +$string['privacy:metadata:lti_submission:userid'] = 'The ID of the user who submitted for the LTI activity'; +$string['privacy:metadata:lti_tool_proxies'] = 'LTI proxies'; +$string['privacy:metadata:lti_tool_proxies:name'] = 'LTI proxy name'; +$string['privacy:metadata:lti_types'] = 'LTI types'; +$string['privacy:metadata:lti_types:name'] = 'LTI type name'; +$string['privacy:metadata:timecreated'] = 'The date at which the record was created'; +$string['privacy:metadata:timemodified'] = 'The date at which the record was modified'; $string['quickgrade'] = 'Allow quick grading'; $string['quickgrade_help'] = 'If enabled, multiple tools can be graded on one page. Add grades and comments then click the "Save all my feedback" button to save all changes for that page.'; $string['redirect'] = 'You will be redirected in few seconds. If you are not, press the button.'; diff --git a/mod/lti/tests/privacy_provider_test.php b/mod/lti/tests/privacy_provider_test.php new file mode 100644 index 00000000000..63a545c3379 --- /dev/null +++ b/mod/lti/tests/privacy_provider_test.php @@ -0,0 +1,313 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_lti + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use mod_lti\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy provider tests class. + * + * @package mod_lti + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_lti_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('mod_lti'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(3, $itemcollection); + + $ltisubmissiontable = array_shift($itemcollection); + $this->assertEquals('lti_submission', $ltisubmissiontable->get_name()); + + $ltitoolproxies = array_shift($itemcollection); + $this->assertEquals('lti_tool_proxies', $ltitoolproxies->get_name()); + + $ltitypestable = array_shift($itemcollection); + $this->assertEquals('lti_types', $ltitypestable->get_name()); + + $privacyfields = $ltisubmissiontable->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('datesubmitted', $privacyfields); + $this->assertArrayHasKey('dateupdated', $privacyfields); + $this->assertArrayHasKey('gradepercent', $privacyfields); + $this->assertArrayHasKey('originalgrade', $privacyfields); + $this->assertEquals('privacy:metadata:lti_submission', $ltisubmissiontable->get_summary()); + + $privacyfields = $ltitoolproxies->get_privacy_fields(); + $this->assertArrayHasKey('name', $privacyfields); + $this->assertArrayHasKey('createdby', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + $this->assertArrayHasKey('timemodified', $privacyfields); + $this->assertEquals('privacy:metadata:lti_tool_proxies', $ltitoolproxies->get_summary()); + + $privacyfields = $ltitypestable->get_privacy_fields(); + $this->assertArrayHasKey('name', $privacyfields); + $this->assertArrayHasKey('createdby', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + $this->assertArrayHasKey('timemodified', $privacyfields); + $this->assertEquals('privacy:metadata:lti_types', $ltitypestable->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + // The LTI activity the user will have submitted something for. + $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); + + // Another LTI activity that has no user activity. + $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); + + // Create a user which will make a submission. + $user = $this->getDataGenerator()->create_user(); + + $this->create_lti_submission($lti->id, $user->id); + + // Check the contexts supplied are correct. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(2, $contextlist); + + $contextformodule = $contextlist->current(); + $cmcontext = context_module::instance($lti->cmid); + $this->assertEquals($cmcontext->id, $contextformodule->id); + + $contextlist->next(); + $contextforsystem = $contextlist->current(); + $this->assertEquals(SYSCONTEXTID, $contextforsystem->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_submissions() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); + + // Create users which will make submissions. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->create_lti_submission($lti->id, $user1->id); + $this->create_lti_submission($lti->id, $user1->id); + $this->create_lti_submission($lti->id, $user2->id); + + // Export all of the data for the context for user 1. + $cmcontext = context_module::instance($lti->cmid); + $this->export_context_data_for_user($user1->id, $cmcontext, 'mod_lti'); + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(2, $data->submissions); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_tool_types() { + $this->resetAfterTest(); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + + // Create a user which will make a tool type. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Create a user that will not make a tool type. + $this->getDataGenerator()->create_user(); + + $type = new stdClass(); + $type->baseurl = 'http://moodle.org'; + $type->course = $course1->id; + lti_add_type($type, new stdClass()); + + $type = new stdClass(); + $type->baseurl = 'http://moodle.org'; + $type->course = $course1->id; + lti_add_type($type, new stdClass()); + + $type = new stdClass(); + $type->baseurl = 'http://moodle.org'; + $type->course = $course2->id; + lti_add_type($type, new stdClass()); + + // Export all of the data for the context. + $coursecontext = context_course::instance($course1->id); + $this->export_context_data_for_user($user->id, $coursecontext, 'mod_lti'); + $writer = \core_privacy\local\request\writer::with_context($coursecontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(2, $data->lti_types); + + $coursecontext = context_course::instance($course2->id); + $this->export_context_data_for_user($user->id, $coursecontext, 'mod_lti'); + $writer = \core_privacy\local\request\writer::with_context($coursecontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(1, $data->lti_types); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_tool_proxies() { + $this->resetAfterTest(); + + // Create a user that will not make a tool proxy. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $toolproxy = new stdClass(); + $toolproxy->createdby = $user; + lti_add_tool_proxy($toolproxy); + + // Export all of the data for the context. + $systemcontext = context_system::instance(); + $this->export_context_data_for_user($user->id, $systemcontext, 'mod_lti'); + $writer = \core_privacy\local\request\writer::with_context($systemcontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(1, $data->lti_tool_proxies); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); + + // Create users that will make submissions. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->create_lti_submission($lti->id, $user1->id); + $this->create_lti_submission($lti->id, $user2->id); + + // Before deletion, we should have 2 responses. + $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); + $this->assertEquals(2, $count); + + // Delete data based on context. + $cmcontext = context_module::instance($lti->cmid); + provider::delete_data_for_all_users_in_context($cmcontext); + + // After deletion, the lti submissions for that lti activity should have been deleted. + $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); + $this->assertEquals(0, $count); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id)); + + // Create users that will make submissions. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->create_lti_submission($lti->id, $user1->id); + $this->create_lti_submission($lti->id, $user2->id); + + // Before deletion we should have 2 responses. + $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]); + $this->assertEquals(2, $count); + + $context = \context_module::instance($lti->cmid); + $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'lti', + [$context->id]); + provider::delete_data_for_user($contextlist); + + // After deletion the lti submission for the first user should have been deleted. + $count = $DB->count_records('lti_submission', ['ltiid' => $lti->id, 'userid' => $user1->id]); + $this->assertEquals(0, $count); + + // Check the submission for the other user is still there. + $ltisubmission = $DB->get_records('lti_submission'); + $this->assertCount(1, $ltisubmission); + $lastsubmission = reset($ltisubmission); + $this->assertEquals($user2->id, $lastsubmission->userid); + } + + /** + * Mimicks the creation of an LTI submission. + * + * There is no API we can use to insert an LTI submission, so we + * will simply insert directly into the database. + * + * @param int $ltiid + * @param int $userid + */ + protected function create_lti_submission(int $ltiid, int $userid) { + global $DB; + + $ltisubmissiondata = [ + 'ltiid' => $ltiid, + 'userid' => $userid, + 'datesubmitted' => time(), + 'dateupdated' => time(), + 'gradepercent' => 65, + 'originalgrade' => 70, + 'launchid' => 3, + 'state' => 1 + ]; + + $DB->insert_record('lti_submission', $ltisubmissiondata); + } +}