From 24707093839eeb49b59ebf84ea9aaa6e49d2ad46 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Tue, 15 May 2018 16:31:06 +0800 Subject: [PATCH] #189 Implemented privacy provider for GDPR compliance --- classes/privacy/provider.php | 237 ++++++++++++++++++ element/bgimage/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_bgimage.php | 1 + element/border/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_border.php | 1 + .../categoryname/classes/privacy/provider.php | 46 ++++ .../en/customcertelement_categoryname.php | 1 + element/code/classes/privacy/provider.php | 46 ++++ .../code/lang/en/customcertelement_code.php | 1 + .../coursename/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_coursename.php | 1 + element/date/classes/privacy/provider.php | 46 ++++ .../date/lang/en/customcertelement_date.php | 1 + .../classes/privacy/provider.php | 46 ++++ .../en/customcertelement_digitalsignature.php | 1 + element/grade/classes/privacy/provider.php | 46 ++++ .../grade/lang/en/customcertelement_grade.php | 1 + .../classes/privacy/provider.php | 46 ++++ .../en/customcertelement_gradeitemname.php | 1 + element/image/classes/privacy/provider.php | 46 ++++ .../image/lang/en/customcertelement_image.php | 1 + .../studentname/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_studentname.php | 1 + .../teachername/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_teachername.php | 1 + element/text/classes/privacy/provider.php | 46 ++++ .../text/lang/en/customcertelement_text.php | 1 + .../userfield/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_userfield.php | 1 + .../userpicture/classes/privacy/provider.php | 46 ++++ .../lang/en/customcertelement_userpicture.php | 1 + lang/en/customcert.php | 6 + tests/privacy_provider_test.php | 207 +++++++++++++++ 33 files changed, 1155 insertions(+) create mode 100644 classes/privacy/provider.php create mode 100644 element/bgimage/classes/privacy/provider.php create mode 100644 element/border/classes/privacy/provider.php create mode 100644 element/categoryname/classes/privacy/provider.php create mode 100644 element/code/classes/privacy/provider.php create mode 100644 element/coursename/classes/privacy/provider.php create mode 100644 element/date/classes/privacy/provider.php create mode 100644 element/digitalsignature/classes/privacy/provider.php create mode 100644 element/grade/classes/privacy/provider.php create mode 100644 element/gradeitemname/classes/privacy/provider.php create mode 100644 element/image/classes/privacy/provider.php create mode 100644 element/studentname/classes/privacy/provider.php create mode 100644 element/teachername/classes/privacy/provider.php create mode 100644 element/text/classes/privacy/provider.php create mode 100644 element/userfield/classes/privacy/provider.php create mode 100644 element/userpicture/classes/privacy/provider.php create mode 100644 tests/privacy_provider_test.php diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..4064474 --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,237 @@ +. + +/** + * Privacy Subsystem implementation for mod_customcert. + * + * @package mod_customcert + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_customcert\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_customcert. + * + * @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) { + $items->add_database_table( + 'customcert_issues', + [ + 'userid' => 'privacy:metadata:customcert_issues:userid', + 'customcertid' => 'privacy:metadata:customcert_issues:customcertid', + 'code' => 'privacy:metadata:customcert_issues:code', + 'emailed' => 'privacy:metadata:customcert_issues:emailed', + 'timecreated' => 'privacy:metadata:customcert_issues:timecreated', + ], + 'privacy:metadata:customcert_issues' + ); + + 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($userid) { + $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 = :modulename + INNER JOIN {customcert} customcert + ON customcert.id = cm.instance + INNER JOIN {customcert_issues} customcertissues + ON customcertissues.customcertid = customcert.id + WHERE customcertissues.userid = :userid"; + + $params = [ + 'modulename' => 'customcert', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + ]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + + 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) { + 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 customcert activities associated with the above course modules. + $customcertidstocmids = self::get_customcert_ids_to_cmids_from_cmids($cmids); + + list($insql, $inparams) = $DB->get_in_or_equal(array_keys($customcertidstocmids), SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['userid' => $user->id]); + $recordset = $DB->get_recordset_select('customcert_issues', "customcertid $insql AND userid = :userid", + $params, 'timecreated, id ASC'); + self::recordset_loop_and_export($recordset, 'customcertid', [], function($carry, $record) { + $carry[] = [ + 'code' => $record->code, + 'emailed' => transform::yesno($record->emailed), + 'timecreated' => transform::datetime($record->timecreated) + ]; + return $carry; + }, function($customcertid, $data) use ($user, $customcertidstocmids) { + $context = \context_module::instance($customcertidstocmids[$customcertid]); + $contextdata = helper::get_context_data($context, $user); + $finaldata = (object) array_merge((array) $contextdata, ['issues' => $data]); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $finaldata); + }); + } + + /** + * 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; + } + + if (!$cm = get_coursemodule_from_id('customcert', $context->instanceid)) { + return; + } + + $DB->delete_records('customcert_issues', ['customcertid' => $cm->instance]); + } + + /** + * 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) { + continue; + } + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('customcert_issues', ['customcertid' => $instanceid, 'userid' => $userid]); + } + } + + /** + * Return a list of Customcert IDs mapped to their course module ID. + * + * @param array $cmids The course module IDs. + * @return array In the form of [$customcertid => $cmid]. + */ + protected static function get_customcert_ids_to_cmids_from_cmids(array $cmids) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + $sql = "SELECT customcert.id, cm.id AS cmid + FROM {customcert} customcert + JOIN {modules} m + ON m.name = :modulename + JOIN {course_modules} cm + ON cm.instance = customcert.id + AND cm.module = m.id + WHERE cm.id $insql"; + $params = array_merge($inparams, ['modulename' => 'customcert']); + + 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/element/bgimage/classes/privacy/provider.php b/element/bgimage/classes/privacy/provider.php new file mode 100644 index 0000000..e29b698 --- /dev/null +++ b/element/bgimage/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_bgimage. + * + * @package customcertelement_bgimage + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_bgimage\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_bgimage 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() { + return 'privacy:metadata'; + } +} diff --git a/element/bgimage/lang/en/customcertelement_bgimage.php b/element/bgimage/lang/en/customcertelement_bgimage.php index ef6357c..467bfa2 100644 --- a/element/bgimage/lang/en/customcertelement_bgimage.php +++ b/element/bgimage/lang/en/customcertelement_bgimage.php @@ -23,3 +23,4 @@ */ $string['pluginname'] = 'Background image'; +$string['privacy:metadata'] = 'The Background image plugin does not store any personal data.'; diff --git a/element/border/classes/privacy/provider.php b/element/border/classes/privacy/provider.php new file mode 100644 index 0000000..1639e53 --- /dev/null +++ b/element/border/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_border. + * + * @package customcertelement_border + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_border\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_border 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() { + return 'privacy:metadata'; + } +} diff --git a/element/border/lang/en/customcertelement_border.php b/element/border/lang/en/customcertelement_border.php index c6098e7..b3e6aed 100644 --- a/element/border/lang/en/customcertelement_border.php +++ b/element/border/lang/en/customcertelement_border.php @@ -23,6 +23,7 @@ */ $string['pluginname'] = 'Border'; +$string['privacy:metadata'] = 'The Border plugin does not store any personal data.'; $string['invalidwidth'] = 'The width has to be a valid number greater than 0.'; $string['width'] = 'Width'; $string['width_help'] = 'Width of the border in mm.'; diff --git a/element/categoryname/classes/privacy/provider.php b/element/categoryname/classes/privacy/provider.php new file mode 100644 index 0000000..30ae33a --- /dev/null +++ b/element/categoryname/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_categoryname. + * + * @package customcertelement_categoryname + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_categoryname\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_categoryname 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() { + return 'privacy:metadata'; + } +} diff --git a/element/categoryname/lang/en/customcertelement_categoryname.php b/element/categoryname/lang/en/customcertelement_categoryname.php index d537cc8..a3c444d 100644 --- a/element/categoryname/lang/en/customcertelement_categoryname.php +++ b/element/categoryname/lang/en/customcertelement_categoryname.php @@ -23,3 +23,4 @@ */ $string['pluginname'] = 'Category name'; +$string['privacy:metadata'] = 'The Category name plugin does not store any personal data.'; diff --git a/element/code/classes/privacy/provider.php b/element/code/classes/privacy/provider.php new file mode 100644 index 0000000..a834da4 --- /dev/null +++ b/element/code/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_code. + * + * @package customcertelement_code + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_code\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_code 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() { + return 'privacy:metadata'; + } +} diff --git a/element/code/lang/en/customcertelement_code.php b/element/code/lang/en/customcertelement_code.php index 03dea67..e67616c 100644 --- a/element/code/lang/en/customcertelement_code.php +++ b/element/code/lang/en/customcertelement_code.php @@ -23,3 +23,4 @@ */ $string['pluginname'] = 'Code'; +$string['privacy:metadata'] = 'The Code plugin does not store any personal data.'; diff --git a/element/coursename/classes/privacy/provider.php b/element/coursename/classes/privacy/provider.php new file mode 100644 index 0000000..12b436c --- /dev/null +++ b/element/coursename/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_coursename. + * + * @package customcertelement_coursename + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_coursename\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_coursename 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() { + return 'privacy:metadata'; + } +} diff --git a/element/coursename/lang/en/customcertelement_coursename.php b/element/coursename/lang/en/customcertelement_coursename.php index 85c3b4a..48781e4 100644 --- a/element/coursename/lang/en/customcertelement_coursename.php +++ b/element/coursename/lang/en/customcertelement_coursename.php @@ -23,3 +23,4 @@ */ $string['pluginname'] = 'Course name'; +$string['privacy:metadata'] = 'The Course name plugin does not store any personal data.'; diff --git a/element/date/classes/privacy/provider.php b/element/date/classes/privacy/provider.php new file mode 100644 index 0000000..cda042d --- /dev/null +++ b/element/date/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_date. + * + * @package customcertelement_date + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_date\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_date 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() { + return 'privacy:metadata'; + } +} diff --git a/element/date/lang/en/customcertelement_date.php b/element/date/lang/en/customcertelement_date.php index 23990c1..366ac6e 100644 --- a/element/date/lang/en/customcertelement_date.php +++ b/element/date/lang/en/customcertelement_date.php @@ -32,6 +32,7 @@ $string['dateitem'] = 'Date item'; $string['dateitem_help'] = 'This will be the date that is printed on the certificate'; $string['issueddate'] = 'Issued date'; $string['pluginname'] = 'Date'; +$string['privacy:metadata'] = 'The Date plugin does not store any personal data.'; $string['numbersuffix_nd_as_in_second'] = 'nd'; $string['numbersuffix_rd_as_in_third'] = 'rd'; $string['numbersuffix_st_as_in_first'] = 'st'; diff --git a/element/digitalsignature/classes/privacy/provider.php b/element/digitalsignature/classes/privacy/provider.php new file mode 100644 index 0000000..d927470 --- /dev/null +++ b/element/digitalsignature/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_digitalsignature. + * + * @package customcertelement_digitalsignature + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_digitalsignature\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_digitalsignature 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() { + return 'privacy:metadata'; + } +} diff --git a/element/digitalsignature/lang/en/customcertelement_digitalsignature.php b/element/digitalsignature/lang/en/customcertelement_digitalsignature.php index 79a51cd..81661f4 100644 --- a/element/digitalsignature/lang/en/customcertelement_digitalsignature.php +++ b/element/digitalsignature/lang/en/customcertelement_digitalsignature.php @@ -25,6 +25,7 @@ $string['digitalsignature'] = 'Digital signature'; $string['nosignature'] = 'No signature'; $string['pluginname'] = 'Digital signature'; +$string['privacy:metadata'] = 'The Digital signature plugin does not store any personal data.'; $string['signaturename'] = 'Signature name'; $string['signaturepassword'] = 'Signature password'; $string['signaturelocation'] = 'Signature location'; diff --git a/element/grade/classes/privacy/provider.php b/element/grade/classes/privacy/provider.php new file mode 100644 index 0000000..052efc2 --- /dev/null +++ b/element/grade/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_grade. + * + * @package customcertelement_grade + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_grade\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_grade 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() { + return 'privacy:metadata'; + } +} diff --git a/element/grade/lang/en/customcertelement_grade.php b/element/grade/lang/en/customcertelement_grade.php index 2d4d4b5..9039329 100644 --- a/element/grade/lang/en/customcertelement_grade.php +++ b/element/grade/lang/en/customcertelement_grade.php @@ -33,3 +33,4 @@ $string['gradepoints'] = 'Points'; $string['gradeletter'] = 'Letter'; $string['pluginname'] = 'Grade'; $string['previewgrade'] = 'Preview grade'; +$string['privacy:metadata'] = 'The Grade plugin does not store any personal data.'; diff --git a/element/gradeitemname/classes/privacy/provider.php b/element/gradeitemname/classes/privacy/provider.php new file mode 100644 index 0000000..d30709f --- /dev/null +++ b/element/gradeitemname/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_gradeitemname. + * + * @package customcertelement_gradeitemname + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_gradeitemname\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_gradeitemname 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() { + return 'privacy:metadata'; + } +} diff --git a/element/gradeitemname/lang/en/customcertelement_gradeitemname.php b/element/gradeitemname/lang/en/customcertelement_gradeitemname.php index 97fada3..f76c40f 100644 --- a/element/gradeitemname/lang/en/customcertelement_gradeitemname.php +++ b/element/gradeitemname/lang/en/customcertelement_gradeitemname.php @@ -25,3 +25,4 @@ $string['gradeitem'] = 'Grade item'; $string['gradeitem_help'] = 'The name of the selected item will be displayed on the PDF.'; $string['pluginname'] = 'Grade item name'; +$string['privacy:metadata'] = 'The Grade item name plugin does not store any personal data.'; diff --git a/element/image/classes/privacy/provider.php b/element/image/classes/privacy/provider.php new file mode 100644 index 0000000..e6db81a --- /dev/null +++ b/element/image/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_image. + * + * @package customcertelement_image + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_image\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_image 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() { + return 'privacy:metadata'; + } +} diff --git a/element/image/lang/en/customcertelement_image.php b/element/image/lang/en/customcertelement_image.php index 659fca8..1422b6b 100644 --- a/element/image/lang/en/customcertelement_image.php +++ b/element/image/lang/en/customcertelement_image.php @@ -28,5 +28,6 @@ $string['image'] = 'Image'; $string['invalidheight'] = 'The height has to be a valid number greater than or equal to 0.'; $string['invalidwidth'] = 'The width has to be a valid number greater than or equal to 0.'; $string['pluginname'] = 'Image'; +$string['privacy:metadata'] = 'The Image plugin does not store any personal data.'; $string['width'] = 'Width'; $string['width_help'] = 'Width of the image in mm. If equal to zero, it is automatically calculated.'; diff --git a/element/studentname/classes/privacy/provider.php b/element/studentname/classes/privacy/provider.php new file mode 100644 index 0000000..0d6d1ad --- /dev/null +++ b/element/studentname/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_studentname. + * + * @package customcertelement_studentname + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_studentname\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_studentname 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() { + return 'privacy:metadata'; + } +} diff --git a/element/studentname/lang/en/customcertelement_studentname.php b/element/studentname/lang/en/customcertelement_studentname.php index 9811fa6..9e6fd3f 100644 --- a/element/studentname/lang/en/customcertelement_studentname.php +++ b/element/studentname/lang/en/customcertelement_studentname.php @@ -23,3 +23,4 @@ */ $string['pluginname'] = 'Student name'; +$string['privacy:metadata'] = 'The Student name plugin does not store any personal data.'; diff --git a/element/teachername/classes/privacy/provider.php b/element/teachername/classes/privacy/provider.php new file mode 100644 index 0000000..9d2ae95 --- /dev/null +++ b/element/teachername/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_teachername. + * + * @package customcertelement_teachername + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_teachername\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_teachername 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() { + return 'privacy:metadata'; + } +} diff --git a/element/teachername/lang/en/customcertelement_teachername.php b/element/teachername/lang/en/customcertelement_teachername.php index fff9204..48265bc 100644 --- a/element/teachername/lang/en/customcertelement_teachername.php +++ b/element/teachername/lang/en/customcertelement_teachername.php @@ -23,5 +23,6 @@ */ $string['pluginname'] = 'Teacher name'; +$string['privacy:metadata'] = 'The Teacher name plugin does not store any personal data.'; $string['teacher'] = 'Teacher'; $string['teacher_help'] = 'This is the teacher name that will be displayed.'; diff --git a/element/text/classes/privacy/provider.php b/element/text/classes/privacy/provider.php new file mode 100644 index 0000000..74ae4d3 --- /dev/null +++ b/element/text/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_text. + * + * @package customcertelement_text + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_text\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_text 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() { + return 'privacy:metadata'; + } +} diff --git a/element/text/lang/en/customcertelement_text.php b/element/text/lang/en/customcertelement_text.php index 81be2e1..077b85a 100644 --- a/element/text/lang/en/customcertelement_text.php +++ b/element/text/lang/en/customcertelement_text.php @@ -23,5 +23,6 @@ */ $string['pluginname'] = 'Text'; +$string['privacy:metadata'] = 'The Text plugin does not store any personal data.'; $string['text'] = 'Text'; $string['text_help'] = 'This is the text that will display on the PDF.'; diff --git a/element/userfield/classes/privacy/provider.php b/element/userfield/classes/privacy/provider.php new file mode 100644 index 0000000..58a385c --- /dev/null +++ b/element/userfield/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_userfield. + * + * @package customcertelement_userfield + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_userfield\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_userfield 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() { + return 'privacy:metadata'; + } +} diff --git a/element/userfield/lang/en/customcertelement_userfield.php b/element/userfield/lang/en/customcertelement_userfield.php index 8095c8c..7b3cb44 100644 --- a/element/userfield/lang/en/customcertelement_userfield.php +++ b/element/userfield/lang/en/customcertelement_userfield.php @@ -23,5 +23,6 @@ */ $string['pluginname'] = 'User field'; +$string['privacy:metadata'] = 'The User field plugin does not store any personal data.'; $string['userfield'] = 'User field'; $string['userfield_help'] = 'This is the user field that will be displayed on the PDF.'; diff --git a/element/userpicture/classes/privacy/provider.php b/element/userpicture/classes/privacy/provider.php new file mode 100644 index 0000000..4d0234f --- /dev/null +++ b/element/userpicture/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_userpicture. + * + * @package customcertelement_userpicture + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_userpicture\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for customcertelement_userpicture 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() { + return 'privacy:metadata'; + } +} diff --git a/element/userpicture/lang/en/customcertelement_userpicture.php b/element/userpicture/lang/en/customcertelement_userpicture.php index b18c323..bd32cb3 100644 --- a/element/userpicture/lang/en/customcertelement_userpicture.php +++ b/element/userpicture/lang/en/customcertelement_userpicture.php @@ -27,5 +27,6 @@ $string['height_help'] = 'Height of the image in mm. If equal to zero, it is aut $string['invalidheight'] = 'The height has to be a valid number greater than or equal to 0.'; $string['invalidwidth'] = 'The width has to be a valid number greater than or equal to 0.'; $string['pluginname'] = 'User picture'; +$string['privacy:metadata'] = 'The User picture plugin does not store any personal data.'; $string['width'] = 'Width'; $string['width_help'] = 'Width of the image in mm. If equal to zero, it is automatically calculated.'; diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 054053d..bb95aa8 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -127,6 +127,12 @@ $string['posx_help'] = 'This is the position in mm from the top left corner you $string['posy'] = 'Position Y'; $string['posy_help'] = 'This is the position in mm from the top left corner you wish the element\'s reference point to locate in the y direction.'; $string['print'] = 'Print'; +$string['privacy:metadata:customcert_issues'] = 'The list of issued certificates'; +$string['privacy:metadata:customcert_issues:code'] = 'The code that belongs to the certificate'; +$string['privacy:metadata:customcert_issues:customcertid'] = 'The ID of the certificate'; +$string['privacy:metadata:customcert_issues:emailed'] = 'Whether or not the certificate was emailed'; +$string['privacy:metadata:customcert_issues:timecreated'] = 'The time the certificate was issued'; +$string['privacy:metadata:customcert_issues:userid'] = 'The ID of the user who was issued the certificate'; $string['rearrangeelements'] = 'Reposition elements'; $string['rearrangeelementsheading'] = 'Drag and drop elements to change where they are positioned on the certificate.'; $string['receiveddate'] = 'Received date'; diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php new file mode 100644 index 0000000..9753b7e --- /dev/null +++ b/tests/privacy_provider_test.php @@ -0,0 +1,207 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_customcert + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_customcert\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy provider tests class. + * + * @package mod_customcert + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_customcert_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + // The customcert activity the user will have an issue from. + $customcert = $this->getDataGenerator()->create_module('customcert', ['course' => $course->id]); + + // Another customcert activity that has no issued certificates. + $this->getDataGenerator()->create_module('customcert', ['course' => $course->id]); + + // Create a user who will be issued a certificate. + $user = $this->getDataGenerator()->create_user(); + + // Issue the certificate. + $this->create_certificate_issue($customcert->id, $user->id); + + // Check the context supplied is correct. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(1, $contextlist); + + $contextformodule = $contextlist->current(); + $cmcontext = context_module::instance($customcert->cmid); + $this->assertEquals($cmcontext->id, $contextformodule->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $customcert = $this->getDataGenerator()->create_module('customcert', array('course' => $course->id)); + + // Create users who will be issued a certificate. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->create_certificate_issue($customcert->id, $user1->id); + $this->create_certificate_issue($customcert->id, $user1->id); + $this->create_certificate_issue($customcert->id, $user2->id); + + // Export all of the data for the context for user 1. + $cmcontext = context_module::instance($customcert->cmid); + $this->export_context_data_for_user($user1->id, $cmcontext, 'mod_customcert'); + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(2, $data->issues); + + $issues = $data->issues; + foreach ($issues as $issue) { + $this->assertArrayHasKey('code', $issue); + $this->assertArrayHasKey('emailed', $issue); + $this->assertArrayHasKey('timecreated', $issue); + } + } + + /** + * 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(); + + $customcert = $this->getDataGenerator()->create_module('customcert', array('course' => $course->id)); + $customcert2 = $this->getDataGenerator()->create_module('customcert', array('course' => $course->id)); + + // Create users who will be issued a certificate. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->create_certificate_issue($customcert->id, $user1->id); + $this->create_certificate_issue($customcert->id, $user2->id); + + $this->create_certificate_issue($customcert2->id, $user1->id); + $this->create_certificate_issue($customcert2->id, $user2->id); + + // Before deletion, we should have 2 issued certificates for the first certificate. + $count = $DB->count_records('customcert_issues', ['customcertid' => $customcert->id]); + $this->assertEquals(2, $count); + + // Delete data based on context. + $cmcontext = context_module::instance($customcert->cmid); + provider::delete_data_for_all_users_in_context($cmcontext); + + // After deletion, the issued certificates for the activity should have been deleted. + $count = $DB->count_records('customcert_issues', ['customcertid' => $customcert->id]); + $this->assertEquals(0, $count); + + // We should still have the issues for the second certificate. + $count = $DB->count_records('customcert_issues', ['customcertid' => $customcert2->id]); + $this->assertEquals(2, $count); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $customcert = $this->getDataGenerator()->create_module('customcert', array('course' => $course->id)); + + // Create users who will be issued a certificate. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->create_certificate_issue($customcert->id, $user1->id); + $this->create_certificate_issue($customcert->id, $user2->id); + + // Before deletion we should have 2 issued certificates. + $count = $DB->count_records('customcert_issues', ['customcertid' => $customcert->id]); + $this->assertEquals(2, $count); + + $context = \context_module::instance($customcert->cmid); + $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'customcert', + [$context->id]); + provider::delete_data_for_user($contextlist); + + // After deletion, the issued certificates for the first user should have been deleted. + $count = $DB->count_records('customcert_issues', ['customcertid' => $customcert->id, 'userid' => $user1->id]); + $this->assertEquals(0, $count); + + // Check the issue for the other user is still there. + $customcertissue = $DB->get_records('customcert_issues'); + $this->assertCount(1, $customcertissue); + $lastissue = reset($customcertissue); + $this->assertEquals($user2->id, $lastissue->userid); + } + + /** + * Mimicks the creation of a customcert issue. + * + * There is no API we can use to insert an customcert issue, so we + * will simply insert directly into the database. + * + * @param int $customcertid + * @param int $userid + */ + protected function create_certificate_issue($customcertid, $userid) { + global $DB; + + static $i = 1; + + $customcertissue = new stdClass(); + $customcertissue->customcertid = $customcertid; + $customcertissue->userid = $userid; + $customcertissue->code = \mod_customcert\certificate::generate_code(); + $customcertissue->timecreated = time() + $i; + + // Insert the record into the database. + $DB->insert_record('customcert_issues', $customcertissue); + + $i++; + } +}