Skip to content

Commit a49f551

Browse files
author
Mihail Geshoski
committed
MDL-62564 privacy: Improve bulk deletion
1 parent d494848 commit a49f551

File tree

13 files changed

+367
-9
lines changed

13 files changed

+367
-9
lines changed

admin/tool/dataprivacy/classes/api.php

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use tool_dataprivacy\local\helper;
4242
use tool_dataprivacy\task\initiate_data_request_task;
4343
use tool_dataprivacy\task\process_data_request_task;
44+
use tool_dataprivacy\data_request;
4445

4546
defined('MOODLE_INTERNAL') || die();
4647

@@ -253,6 +254,8 @@ public static function create_data_request($foruser, $type, $comments = '',
253254
$datarequest->set('type', $type);
254255
// Set request comments.
255256
$datarequest->set('comments', $comments);
257+
// Set the creation method.
258+
$datarequest->set('creationmethod', $creationmethod);
256259

257260
// Store subject access request.
258261
$datarequest->create();
@@ -275,14 +278,16 @@ public static function create_data_request($foruser, $type, $comments = '',
275278
* @param int $userid The User ID.
276279
* @param int[] $statuses The status filters.
277280
* @param int[] $types The request type filters.
281+
* @param int[] $creationmethods The request creation method filters.
278282
* @param string $sort The order by clause.
279283
* @param int $offset Amount of records to skip.
280284
* @param int $limit Amount of records to fetch.
281285
* @return data_request[]
282286
* @throws coding_exception
283287
* @throws dml_exception
284288
*/
285-
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
289+
public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
290+
$sort = '', $offset = 0, $limit = 0) {
286291
global $DB, $USER;
287292
$results = [];
288293
$sqlparams = [];
@@ -306,6 +311,13 @@ public static function get_data_requests($userid = 0, $statuses = [], $types = [
306311
$sqlparams = array_merge($sqlparams, $typeparams);
307312
}
308313

314+
// Set request creation method filter.
315+
if (!empty($creationmethods)) {
316+
list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
317+
$sqlconditions[] = "creationmethod $typeinsql";
318+
$sqlparams = array_merge($sqlparams, $typeparams);
319+
}
320+
309321
if ($userid) {
310322
// Get the data requests for the user or data requests made by the user.
311323
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
@@ -348,7 +360,7 @@ public static function get_data_requests($userid = 0, $statuses = [], $types = [
348360

349361
if (!empty($expiredrequests)) {
350362
data_request::expire($expiredrequests);
351-
$results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
363+
$results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
352364
}
353365
}
354366

@@ -361,11 +373,12 @@ public static function get_data_requests($userid = 0, $statuses = [], $types = [
361373
* @param int $userid The User ID.
362374
* @param int[] $statuses The status filters.
363375
* @param int[] $types The request type filters.
376+
* @param int[] $creationmethods The request creation method filters.
364377
* @return int
365378
* @throws coding_exception
366379
* @throws dml_exception
367380
*/
368-
public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
381+
public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
369382
global $DB, $USER;
370383
$count = 0;
371384
$sqlparams = [];
@@ -379,6 +392,11 @@ public static function get_data_requests_count($userid = 0, $statuses = [], $typ
379392
$sqlconditions[] = "type $typeinsql";
380393
$sqlparams = array_merge($sqlparams, $typeparams);
381394
}
395+
if (!empty($creationmethods)) {
396+
list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
397+
$sqlconditions[] = "creationmethod $typeinsql";
398+
$sqlparams = array_merge($sqlparams, $typeparams);
399+
}
382400
if ($userid) {
383401
// Get the data requests for the user or data requests made by the user.
384402
$sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
@@ -962,6 +980,34 @@ public static function get_effective_contextlevel_purpose($contextlevel, $forced
962980
return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
963981
}
964982

983+
/**
984+
* Creates an expired context record for the provided context id.
985+
*
986+
* @param int $contextid
987+
* @return \tool_dataprivacy\expired_context
988+
*/
989+
public static function create_expired_context($contextid) {
990+
$record = (object)[
991+
'contextid' => $contextid,
992+
'status' => expired_context::STATUS_EXPIRED,
993+
];
994+
$expiredctx = new expired_context(0, $record);
995+
$expiredctx->save();
996+
997+
return $expiredctx;
998+
}
999+
1000+
/**
1001+
* Deletes an expired context record.
1002+
*
1003+
* @param int $id The tool_dataprivacy_ctxexpire id.
1004+
* @return bool True on success.
1005+
*/
1006+
public static function delete_expired_context($id) {
1007+
$expiredcontext = new expired_context($id);
1008+
return $expiredcontext->delete();
1009+
}
1010+
9651011
/**
9661012
* Updates the status of an expired context.
9671013
*

admin/tool/dataprivacy/classes/data_request.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
* @copyright 2018 Jun Pataleta
2222
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2323
*/
24+
2425
namespace tool_dataprivacy;
26+
2527
defined('MOODLE_INTERNAL') || die();
2628

2729
use core\persistent;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
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.
8+
//
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.
13+
//
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/>.
16+
17+
/**
18+
* Event observers supported by this module.
19+
*
20+
* @package tool_dataprivacy
21+
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
22+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
25+
namespace tool_dataprivacy\event;
26+
27+
use \tool_dataprivacy\api;
28+
use \tool_dataprivacy\data_request;
29+
30+
defined('MOODLE_INTERNAL') || die();
31+
32+
/**
33+
* Event observers supported by this module.
34+
*
35+
* @package tool_dataprivacy
36+
* @copyright 2018 Mihail Geshoski <mihail@moodle.com>
37+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38+
*/
39+
class user_deleted_observer {
40+
41+
/**
42+
* Create user data deletion request when the user is deleted.
43+
*
44+
* @param \core\event\user_deleted $event
45+
*/
46+
public static function create_delete_data_request(\core\event\user_deleted $event) {
47+
// Automatic creation of deletion requests must be enabled.
48+
if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) {
49+
$requesttypes = [api::DATAREQUEST_TYPE_DELETE];
50+
$requeststatuses = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DELETED];
51+
52+
$hasongoingdeleterequests = api::has_ongoing_request($event->objectid, $requesttypes[0]);
53+
$hascompleteddeleterequest = (api::get_data_requests_count($event->objectid, $requeststatuses,
54+
$requesttypes) > 0) ? true : false;
55+
56+
if (!$hasongoingdeleterequests && !$hascompleteddeleterequest) {
57+
api::create_data_request($event->objectid, $requesttypes[0],
58+
get_string('datarequestcreatedupondelete', 'tool_dataprivacy'),
59+
data_request::DATAREQUEST_CREATION_AUTO);
60+
}
61+
}
62+
}
63+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
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.
8+
//
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.
13+
//
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/>.
16+
17+
/**
18+
* Expired contexts manager for CONTEXT_USER.
19+
*
20+
* @package tool_dataprivacy
21+
* @copyright 2018 David Monllao
22+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
namespace tool_dataprivacy;
25+
26+
use core_privacy\manager;
27+
28+
defined('MOODLE_INTERNAL') || die();
29+
30+
/**
31+
* Expired contexts manager for CONTEXT_USER.
32+
*
33+
* @copyright 2018 David Monllao
34+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35+
*/
36+
class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
37+
38+
/**
39+
* Only user level.
40+
*
41+
* @return int[]
42+
*/
43+
protected function get_context_levels() {
44+
return [CONTEXT_USER];
45+
}
46+
47+
/**
48+
* Returns the user context instances that are expired.
49+
*
50+
* @return \stdClass[]
51+
*/
52+
protected function get_expired_contexts() {
53+
global $DB;
54+
55+
// Including context info + last login timestamp.
56+
$fields = 'ctx.id AS id, ' . \context_helper::get_preload_record_columns_sql('ctx');
57+
58+
$purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
59+
60+
// Calculate what is considered expired according to the context level effective purpose (= now + retention period).
61+
$expiredtime = new \DateTime();
62+
$retention = new \DateInterval($purpose->get('retentionperiod'));
63+
$expiredtime->sub($retention);
64+
65+
$sql = "SELECT $fields FROM {context} ctx
66+
JOIN {user} u ON ctx.contextlevel = ? AND ctx.instanceid = u.id
67+
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid
68+
WHERE u.lastaccess <= ? AND u.lastaccess > 0 AND expiredctx.id IS NULL
69+
ORDER BY ctx.path, ctx.contextlevel ASC";
70+
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_USER, $expiredtime->getTimestamp()]);
71+
72+
$expiredcontexts = [];
73+
foreach ($possiblyexpired as $record) {
74+
75+
\context_helper::preload_from_record($record);
76+
77+
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
78+
// module contexts below it will not exist).
79+
$context = \context::instance_by_id($record->id, false);
80+
if (!$context) {
81+
continue;
82+
}
83+
84+
if (is_siteadmin($context->instanceid)) {
85+
continue;
86+
}
87+
88+
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
89+
foreach ($courses as $course) {
90+
if (!$course->enddate) {
91+
// We can not know it what is going on here, so we prefer to be conservative.
92+
continue 2;
93+
}
94+
95+
if ($course->enddate >= time()) {
96+
// Future or ongoing course.
97+
continue 2;
98+
}
99+
}
100+
101+
$expiredcontexts[$context->id] = $context;
102+
}
103+
104+
return $expiredcontexts;
105+
}
106+
107+
/**
108+
* Deletes user data from the provided context.
109+
*
110+
* Overwritten to delete the user.
111+
*
112+
* @param manager $privacymanager
113+
* @param expired_context $expiredctx
114+
* @return \context|false
115+
*/
116+
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
117+
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
118+
if (!$context) {
119+
return false;
120+
}
121+
122+
if (!PHPUNIT_TEST) {
123+
mtrace('Deleting context ' . $context->id . ' - ' .
124+
shorten_text($context->get_context_name(true, true)));
125+
}
126+
127+
// To ensure that all user data is deleted, instead of deleting by context, we run through and collect any stray
128+
// contexts for the user that may still exist and call delete_data_for_user().
129+
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
130+
$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
131+
$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
132+
133+
foreach ($contextlistcollection as $contextlist) {
134+
$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
135+
$user,
136+
$contextlist->get_component(),
137+
$contextlist->get_contextids()
138+
));
139+
}
140+
141+
$privacymanager->delete_data_for_user($approvedlistcollection);
142+
api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
143+
144+
// Delete the user.
145+
delete_user($user);
146+
147+
return $context;
148+
}
149+
}

0 commit comments

Comments
 (0)