diff --git a/admin/tool/dataprivacy/classes/api.php b/admin/tool/dataprivacy/classes/api.php index 9fca0960243..4d2f8e9ac6a 100644 --- a/admin/tool/dataprivacy/classes/api.php +++ b/admin/tool/dataprivacy/classes/api.php @@ -28,6 +28,8 @@ use context_system; use core\invalid_persistent_exception; use core\message\message; use core\task\manager; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist_collection; use core_user; use dml_exception; use moodle_exception; @@ -338,6 +340,11 @@ class api { // Update the status and the DPO. $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id); + // Approve all the contexts attached to the request. + // Currently, approving the request implicitly approves all associated contexts, but this may change in future, allowing + // users to selectively approve certain contexts only. + self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED); + // Fire an ad hoc task to initiate the data request process. $task = new process_data_request_task(); $task->set_custom_data(['requestid' => $requestid]); @@ -753,4 +760,114 @@ class api { $expiredctx->set('status', $status); $expiredctx->save(); } + + /** + * Adds the contexts from the contextlist_collection to the request with the status provided. + * + * @param contextlist_collection $clcollection a collection of contextlists for all components. + * @param int $requestid the id of the request. + * @param int $status the status to set the contexts to. + */ + public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) { + foreach ($clcollection as $contextlist) { + // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it. + $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist); + $clp->create(); + $contextlistid = $clp->get('id'); + + // Store the associated contexts in the contextlist. + foreach ($contextlist->get_contextids() as $contextid) { + $context = new contextlist_context(); + $context->set('contextid', $contextid) + ->set('contextlistid', $contextlistid) + ->set('status', $status) + ->create(); + } + + // Create the relation to the request. + $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid); + $requestcontextlist->create(); + } + } + + /** + * Sets the status of all contexts associated with the request. + * + * @param int $requestid the requestid to which the contexts belong. + * @param int $status the status to set to. + * @throws \dml_exception if the requestid is invalid. + * @throws \moodle_exception if the status is invalid. + */ + public static function update_request_contexts_with_status(int $requestid, int $status) { + // Validate contextlist_context status using the persistent's attribute validation. + $contextlistcontext = new contextlist_context(); + $contextlistcontext->set('status', $status); + if (array_key_exists('status', $contextlistcontext->get_errors())) { + throw new moodle_exception("Invalid contextlist_context status: $status"); + } + + // Validate requestid using the persistent's record validation. + // A dml_exception is thrown if the record is missing. + $datarequest = new data_request($requestid); + + // Bulk update the status of the request contexts. + global $DB; + + $select = "SELECT ctx.id as id + FROM {" . request_contextlist::TABLE . "} rcl + JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id + JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid + WHERE rcl.requestid = ?"; + + $update = "UPDATE {" . contextlist_context::TABLE . "} + SET status = ? + WHERE id IN ({$select})"; + $DB->execute($update, [$status, $requestid]); + } + + /** + * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection. + * + * @param data_request $request the data request with which the contextlists are associated. + * @return contextlist_collection the collection of approved_contextlist objects. + */ + public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection { + $foruser = core_user::get_user($request->get('userid')); + + // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here. + global $DB; + $sql = "SELECT cl.component, ctx.contextid + FROM {" . request_contextlist::TABLE . "} rcl + JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id + JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid + WHERE rcl.requestid = ? + AND ctx.status = ? + ORDER BY cl.component, ctx.contextid"; + + // Create the approved contextlist collection object. + $lastcomponent = null; + $approvedcollection = new contextlist_collection($foruser->id); + + $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]); + foreach ($rs as $record) { + // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the + // last (the one we've just finished with) and reset the context array for the next one. + if ($lastcomponent != $record->component) { + if (!empty($contexts)) { + $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts)); + } + $contexts = []; + } + $contexts[] = $record->contextid; + $lastcomponent = $record->component; + } + $rs->close(); + + // The data for the last component contextlist won't have been written yet, so write it now. + if (!empty($contexts)) { + $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts)); + } + + return $approvedcollection; + } } diff --git a/admin/tool/dataprivacy/classes/contextlist.php b/admin/tool/dataprivacy/classes/contextlist.php new file mode 100644 index 00000000000..ef25c11b9a3 --- /dev/null +++ b/admin/tool/dataprivacy/classes/contextlist.php @@ -0,0 +1,64 @@ +. + +/** + * Contains the contextlist persistent. + * + * @package tool_dataprivacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * The contextlist persistent. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist extends persistent { + + /** The table name this persistent object maps to. */ + const TABLE = 'tool_dataprivacy_contextlist'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'component' => [ + 'type' => PARAM_TEXT + ] + ]; + } + + /** + * Create a new contextlist persistent from an instance of \core_privacy\local\request\contextlist. + * + * @param \core_privacy\local\request\contextlist $contextlist the core privacy contextlist. + * @return contextlist a contextlist persistent. + */ + public static function from_contextlist(\core_privacy\local\request\contextlist $contextlist) : contextlist { + $contextlistpersistent = new contextlist(); + return $contextlistpersistent->set('component', $contextlist->get_component()); + } +} diff --git a/admin/tool/dataprivacy/classes/contextlist_context.php b/admin/tool/dataprivacy/classes/contextlist_context.php new file mode 100644 index 00000000000..0f4ca9fcff7 --- /dev/null +++ b/admin/tool/dataprivacy/classes/contextlist_context.php @@ -0,0 +1,74 @@ +. + +/** + * Contains the contextlist_context persistent. + * + * @package tool_dataprivacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * The contextlist_context persistent. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist_context extends persistent { + + /** The table name this persistent object maps to. */ + const TABLE = 'tool_dataprivacy_ctxlst_ctx'; + + /** This context is pending approval. */ + const STATUS_PENDING = 0; + + /** This context has been approved. */ + const STATUS_APPROVED = 1; + + /** This context has been rejected. */ + const STATUS_REJECTED = 2; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'contextid' => [ + 'type' => PARAM_INT + ], + 'contextlistid' => [ + 'type' => PARAM_INT + ], + 'status' => [ + 'choices' => [ + self::STATUS_PENDING, + self::STATUS_APPROVED, + self::STATUS_REJECTED, + ], + 'default' => self::STATUS_PENDING, + 'type' => PARAM_INT + ] + ]; + } +} diff --git a/admin/tool/dataprivacy/classes/request_contextlist.php b/admin/tool/dataprivacy/classes/request_contextlist.php new file mode 100644 index 00000000000..bd757426b42 --- /dev/null +++ b/admin/tool/dataprivacy/classes/request_contextlist.php @@ -0,0 +1,70 @@ +. + +/** + * Contains the request_contextlist persistent. + * + * @package tool_dataprivacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * The request_contextlist persistent. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class request_contextlist extends persistent { + + /** The table name this persistent object maps to. */ + const TABLE = 'tool_dataprivacy_rqst_ctxlst'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'requestid' => [ + 'type' => PARAM_INT + ], + 'contextlistid' => [ + 'type' => PARAM_INT + ] + ]; + } + + /** + * Creates a new relation, but does not persist it. + * + * @param $requestid + * @param $contextlistid + * @return $this + * @throws \coding_exception + */ + public static function create_relation($requestid, $contextlistid) { + $requestcontextlist = new request_contextlist(); + return $requestcontextlist->set('requestid', $requestid) + ->set('contextlistid', $contextlistid); + } +} diff --git a/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php b/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php index e6baa049951..4e62b74fa7e 100644 --- a/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php +++ b/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Adhoc task that processes a data request and prepares the user's metadata for review. + * Adhoc task that processes a data request and prepares the user's relevant contexts for review. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta @@ -28,12 +28,13 @@ use coding_exception; use core\task\adhoc_task; use moodle_exception; use tool_dataprivacy\api; +use tool_dataprivacy\contextlist_context; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** - * Class that processes a data request and prepares the user's metadata for review. + * Class that processes a data request and prepares the user's relevant contexts for review. * * Custom data accepted: * - requestid -> The ID of the data request to be processed. @@ -70,14 +71,17 @@ class initiate_data_request_task extends adhoc_task { } // Update the status of this request as pre-processing. - mtrace('Generating user metadata...'); + mtrace('Generating the contexts containing personal data for the user...'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_PREPROCESSING); - // TODO: Add code here to process the request and prepare the metadata to for review. + // Add the list of relevant contexts to the request, and mark all as pending approval. + $privacymanager = new \core_privacy\manager(); + $contextlistcollection = $privacymanager->get_contexts_for_userid($datarequest->get('userid')); + api::add_request_contexts_with_status($contextlistcollection, $requestid, contextlist_context::STATUS_PENDING); - // When the preparation of the metadata finishes, update the request status to awaiting approval. + // When the preparation of the contexts finishes, update the request status to awaiting approval. api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); - mtrace('User metadata generation complete...'); + mtrace('Context generation complete...'); // Get the list of the site Data Protection Officers. $dpos = api::get_site_dpos(); diff --git a/admin/tool/dataprivacy/classes/task/process_data_request_task.php b/admin/tool/dataprivacy/classes/task/process_data_request_task.php index 19435a293ad..1b5a60f688a 100644 --- a/admin/tool/dataprivacy/classes/task/process_data_request_task.php +++ b/admin/tool/dataprivacy/classes/task/process_data_request_task.php @@ -82,15 +82,13 @@ class process_data_request_task extends adhoc_task { api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING); if ($request->type == api::DATAREQUEST_TYPE_EXPORT) { - // TODO: Update this code to retrieve the approved_contextlist properly. + // Get the collection of approved_contextlist objects needed for core_privacy data export. + $approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent); + + // Export the data. $manager = new \core_privacy\manager(); - $contextcollection = $manager->get_contexts_for_userid($foruser->id); - $approvedcollection = new \core_privacy\local\request\contextlist_collection($foruser->id); - foreach ($contextcollection as $contextlist) { - $approvedcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist($foruser, - $contextlist->get_component(), $contextlist->get_contextids())); - } - $exportedcontent = $manager->export_user_data($approvedcollection); + $exportedcontent = $manager->export_user_data($approvedclcollection); + $fs = get_file_storage(); $filerecord = new \stdClass; $filerecord->component = 'tool_dataprivacy'; @@ -106,15 +104,12 @@ class process_data_request_task extends adhoc_task { $thing = $fs->create_file_from_pathname($filerecord, $exportedcontent); } else if ($request->type == api::DATAREQUEST_TYPE_DELETE) { - // TODO: Update this code to retrieve the approved_contextlist properly. + // Get the collection of approved_contextlist objects needed for core_privacy data deletion. + $approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent); + + // Delete the data $manager = new \core_privacy\manager(); - $contextcollection = $manager->get_contexts_for_userid($foruser->id); - $approvedcollection = new \core_privacy\local\request\contextlist_collection($foruser->id); - foreach ($contextcollection as $contextlist) { - $approvedcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist($foruser, - $contextlist->get_component(), $contextlist->get_contextids())); - } - $manager->delete_user_data($approvedcollection); + $manager->delete_user_data($approvedclcollection); } // When the preparation of the metadata finishes, update the request status to awaiting approval. @@ -150,7 +145,8 @@ class process_data_request_task extends adhoc_task { // Message to the recipient. $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', $SITE->fullname); // Prepare download link. - $downloadurl = new moodle_url('#'); // TODO: Replace with the proper download URL. + $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(), + $thing->get_filepath(), $thing->get_filename(), true); $downloadlink = new action_link($downloadurl, get_string('download', 'tool_dataprivacy')); $messagetextdata['downloadlink'] = $downloadlink->export_for_template($output); break; diff --git a/admin/tool/dataprivacy/db/install.xml b/admin/tool/dataprivacy/db/install.xml index be5e889df40..90abc24bbc6 100644 --- a/admin/tool/dataprivacy/db/install.xml +++ b/admin/tool/dataprivacy/db/install.xml @@ -104,5 +104,43 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
diff --git a/admin/tool/dataprivacy/lib.php b/admin/tool/dataprivacy/lib.php index 0db8244e277..3c66485ef3c 100644 --- a/admin/tool/dataprivacy/lib.php +++ b/admin/tool/dataprivacy/lib.php @@ -185,7 +185,29 @@ function tool_dataprivacy_output_fragment_contextlevel_form($args) { * @return bool Returns false if we don't find a file. */ function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + global $USER; + if ($context->contextlevel == CONTEXT_USER) { + // Make sure the user is logged in. + require_login(null, false); + + // Validate the user downloading this archive. + $usercontext = context_user::instance($USER->id); + // The user downloading this is not the user the archive has been prepared for. Check if it's the requester (e.g. parent). + if ($usercontext->instanceid !== $context->instanceid) { + // Get the data request ID. This should be the first element of the $args array. + $itemid = $args[0]; + // Fetch the data request object. An invalid ID will throw an exception. + $datarequest = new \tool_dataprivacy\data_request($itemid); + + // Check if the user is the requester and has the capability to make data requests for the target user. + $candownloadforuser = has_capability('tool/dataprivacy:makedatarequestsforchildren', $context); + if ($USER->id != $datarequest->get('requestedby') || !$candownloadforuser) { + return false; + } + } + + // All good. Serve the exported data. $fs = get_file_storage(); $relativepath = implode('/', $args); $fullpath = "/$context->id/tool_dataprivacy/$filearea/$relativepath"; diff --git a/admin/tool/dataprivacy/templates/request_details.mustache b/admin/tool/dataprivacy/templates/request_details.mustache index 6f00e12a950..531e3947b98 100644 --- a/admin/tool/dataprivacy/templates/request_details.mustache +++ b/admin/tool/dataprivacy/templates/request_details.mustache @@ -71,7 +71,7 @@ {{#canreview}} - {{#str}}reviewdata, tool_dataprivacy{{/str}} + {{/canreview}}