2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Adhoc task that processes an approved data request and prepares/deletes the user's data.
20 * @package tool_dataprivacy
21 * @copyright 2018 Jun Pataleta
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 namespace tool_dataprivacy\task;
29 use core\message\message;
30 use core\task\adhoc_task;
34 use tool_dataprivacy\api;
35 use tool_dataprivacy\data_request;
37 defined('MOODLE_INTERNAL') || die();
40 * Class that processes an approved data request and prepares/deletes the user's data.
42 * Custom data accepted:
43 * - requestid -> The ID of the data request to be processed.
45 * @package tool_dataprivacy
46 * @copyright 2018 Jun Pataleta
47 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49 class process_data_request_task extends adhoc_task {
52 * Run the task to initiate the data request process.
54 * @throws coding_exception
55 * @throws moodle_exception
57 public function execute() {
58 global $CFG, $PAGE, $SITE;
60 require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
62 if (!isset($this->get_custom_data()->requestid)) {
63 throw new coding_exception('The custom data \'requestid\' is required.');
65 $requestid = $this->get_custom_data()->requestid;
67 $requestpersistent = new data_request($requestid);
68 $request = $requestpersistent->to_record();
70 // Check if this request still needs to be processed. e.g. The user might have cancelled it before this task has run.
71 $status = $requestpersistent->get('status');
72 if (!api::is_active($status)) {
73 mtrace("Request {$requestid} with status {$status} doesn't need to be processed. Skipping...");
77 // If no site purpose is defined, reject requests since they cannot be processed.
78 if (!\tool_dataprivacy\data_registry::defaults_set()) {
79 api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED);
80 mtrace('No site purpose defined. Request ' . $requestid . ' rejected.');
85 // We set an observer against it to handle failures.
86 $manager = new \core_privacy\manager();
87 $manager->set_observer(new \tool_dataprivacy\manager_observer());
89 // Get the user details now. We might not be able to retrieve it later if it's a deletion processing.
90 $foruser = core_user::get_user($request->userid);
92 // Update the status of this request as pre-processing.
93 mtrace('Pre-processing request...');
94 api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING);
95 $contextlistcollection = $manager->get_contexts_for_userid($requestpersistent->get('userid'));
97 mtrace('Fetching approved contextlists from collection');
98 $approvedclcollection = api::get_approved_contextlist_collection_for_collection(
99 $contextlistcollection, $foruser, $request->type);
101 mtrace('Processing request...');
102 $completestatus = api::DATAREQUEST_STATUS_COMPLETE;
104 if ($request->type == api::DATAREQUEST_TYPE_EXPORT) {
105 // Get the user context.
106 $usercontext = \context_user::instance($foruser->id, IGNORE_MISSING);
108 mtrace("Request {$requestid} cannot be processed due to a missing user context instance for the user
109 with ID {$foruser->id}. Skipping...");
114 $exportedcontent = $manager->export_user_data($approvedclcollection);
116 $fs = get_file_storage();
117 $filerecord = new \stdClass;
118 $filerecord->component = 'tool_dataprivacy';
119 $filerecord->contextid = $usercontext->id;
120 $filerecord->userid = $foruser->id;
121 $filerecord->filearea = 'export';
122 $filerecord->filename = 'export.zip';
123 $filerecord->filepath = '/';
124 $filerecord->itemid = $requestid;
125 $filerecord->license = $CFG->sitedefaultlicense;
126 $filerecord->author = fullname($foruser);
128 $thing = $fs->create_file_from_pathname($filerecord, $exportedcontent);
129 $completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY;
130 } else if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
132 $manager = new \core_privacy\manager();
133 $manager->set_observer(new \tool_dataprivacy\manager_observer());
135 $manager->delete_data_for_user($approvedclcollection);
136 $completestatus = api::DATAREQUEST_STATUS_DELETED;
139 // When the preparation of the metadata finishes, update the request status to awaiting approval.
140 api::update_request_status($requestid, $completestatus);
141 mtrace('The processing of the user data request has been completed...');
143 // Create message to notify the user regarding the processing results.
144 $dpo = core_user::get_user($request->dpo);
145 $message = new message();
146 $message->courseid = $SITE->id;
147 $message->component = 'tool_dataprivacy';
148 $message->name = 'datarequestprocessingresults';
149 $message->userfrom = $dpo;
150 $message->replyto = $dpo->email;
151 $message->replytoname = fullname($dpo->email);
154 // Prepare the context data for the email message body.
156 'username' => fullname($foruser)
159 $output = $PAGE->get_renderer('tool_dataprivacy');
162 switch ($request->type) {
163 case api::DATAREQUEST_TYPE_EXPORT:
164 // Check if the user is allowed to download their own export. (This is for
165 // institutions which centrally co-ordinate subject access request across many
166 // systems, not just one Moodle instance, so we don't want every instance emailing
168 if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) {
172 $typetext = get_string('requesttypeexport', 'tool_dataprivacy');
173 // We want to notify the user in Moodle about the processing results.
174 $message->notification = 1;
175 $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php');
176 $message->contexturl = $datarequestsurl;
177 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
178 // Message to the recipient.
179 $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', $SITE->fullname);
180 // Prepare download link.
181 $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(),
182 $thing->get_filepath(), $thing->get_filename(), true);
183 $downloadlink = new action_link($downloadurl, get_string('download', 'tool_dataprivacy'));
184 $messagetextdata['downloadlink'] = $downloadlink->export_for_template($output);
186 case api::DATAREQUEST_TYPE_DELETE:
187 $typetext = get_string('requesttypedelete', 'tool_dataprivacy');
188 // No point notifying a deleted user in Moodle.
189 $message->notification = 0;
190 // Message to the recipient.
191 $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy', $SITE->fullname);
192 // Message will be sent to the deleted user via email only.
196 throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
199 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
200 $message->subject = $subject;
201 $message->fullmessageformat = FORMAT_HTML;
202 $message->userto = $foruser;
204 // Render message email body.
205 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata);
206 $message->fullmessage = html_to_text($messagehtml);
207 $message->fullmessagehtml = $messagehtml;
209 // Send message to the user involved.
211 $messagesent = false;
213 // Do not sent an email if the user has been deleted. The user email has been previously deleted.
214 if (!$foruser->deleted) {
215 $messagesent = email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
218 $messagesent = message_send($message);
222 mtrace('Message sent to user: ' . $messagetextdata['username']);
226 // Send to requester as well in some circumstances.
227 if ($foruser->id != $request->requestedby) {
228 $sendtorequester = false;
229 switch ($request->type) {
230 case api::DATAREQUEST_TYPE_EXPORT:
231 // Send to the requester as well if they can download it, unless they are the
232 // DPO. If we didn't notify the user themselves (because they can't download)
233 // then send to requester even if it is the DPO, as in that case the requester
234 // needs to take some action.
235 if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) {
236 $sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby);
239 case api::DATAREQUEST_TYPE_DELETE:
240 // Send to the requester if they are not the DPO and if they are allowed to
241 // create data requests for the user (e.g. Parent).
242 $sendtorequester = !api::is_site_dpo($request->requestedby) &&
243 api::can_create_data_request_for_user($request->userid, $request->requestedby);
246 throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
249 // Ensure the requester has the capability to make data requests for this user.
250 if ($sendtorequester) {
251 $requestedby = core_user::get_user($request->requestedby);
252 $message->userto = $requestedby;
253 $messagetextdata['username'] = fullname($requestedby);
254 // Render message email body.
255 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata);
256 $message->fullmessage = html_to_text($messagehtml);
257 $message->fullmessagehtml = $messagehtml;
261 email_to_user($requestedby, $dpo, $subject, $message->fullmessage, $messagehtml);
263 message_send($message);
265 mtrace('Message sent to requester: ' . $messagetextdata['username']);
269 if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
271 delete_user($foruser);