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;
// 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]);
$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;
+ }
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the contextlist persistent.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @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 <jrhdallimore@gmail.com>
+ * @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());
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the contextlist_context persistent.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @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 <jrhdallimore@gmail.com>
+ * @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
+ ]
+ ];
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the request_contextlist persistent.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @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 <jrhdallimore@gmail.com>
+ * @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);
+ }
+}
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * 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
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.
}
// 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();
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';
$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.
// 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;
<KEY NAME="contextid" TYPE="foreign-unique" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
</TABLE>
+ <TABLE NAME="tool_dataprivacy_contextlist" COMMENT="List of contexts for a component">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+ <FIELD NAME="component" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Frankenstyle component name"/>
+ <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ </KEYS>
+ </TABLE>
+ <TABLE NAME="tool_dataprivacy_ctxlst_ctx" COMMENT="A contextlist context item">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+ <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="contextlistid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Approval status of the context item"/>
+ <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ <KEY NAME="contextlistid" TYPE="foreign" FIELDS="contextlistid" REFTABLE="tool_dataprivacy_contextlist" REFFIELDS="id" COMMENT="Reference to the contextlist containing this context item"/>
+ </KEYS>
+ </TABLE>
+ <TABLE NAME="tool_dataprivacy_rqst_ctxlst" COMMENT="Association table joining requests and contextlists">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+ <FIELD NAME="requestid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+ <FIELD NAME="contextlistid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ <KEY NAME="requestid" TYPE="foreign" FIELDS="requestid" REFTABLE="tool_dataprivacy_request" REFFIELDS="id" COMMENT="Reference to the request"/>
+ <KEY NAME="contextlistid" TYPE="foreign" FIELDS="contextlistid" REFTABLE="tool_dataprivacy_contextlist" REFFIELDS="id" COMMENT="Reference to the contextlist"/>
+ <KEY NAME="request_contextlist" TYPE="unique" FIELDS="requestid, contextlistid" COMMENT="Uniqueness constraint on request and contextlist"/>
+ </KEYS>
+ </TABLE>
</TABLES>
</XMLDB>
* @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";
</span>
</div>
{{#canreview}}
- <a href="{{reviewurl}}" class="btn btn-default">{{#str}}reviewdata, tool_dataprivacy{{/str}}</a>
+ <!--a href="{{reviewurl}}" class="btn btn-default">{{#str}}reviewdata, tool_dataprivacy{{/str}}</a-->
{{/canreview}}
</div>
</div>