Merge branch 'MDL-63401-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE

This commit is contained in:
Eloy Lafuente (stronk7)
2018-10-09 11:36:58 +02:00
18 changed files with 2975 additions and 981 deletions
+20 -75
View File
@@ -244,14 +244,6 @@ class api {
if (self::is_site_dpo($requestinguser)) {
// The user making the request is a DPO. Should be fine.
$datarequest->set('dpo', $requestinguser);
} else {
// If not a DPO, only users with the capability to make data requests for the user should be allowed.
// (e.g. users with the Parent role, etc).
if (!self::can_create_data_request_for_user($foruser)) {
$forusercontext = \context_user::instance($foruser);
throw new required_capability_exception($forusercontext,
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
}
}
}
// The user making the request.
@@ -664,16 +656,31 @@ class api {
/**
* Checks whether a non-DPO user can make a data request for another user.
*
* @param int $user The user ID of the target user.
* @param int $requester The user ID of the user making the request.
* @return bool
* @throws coding_exception
* @param int $user The user ID of the target user.
* @param int $requester The user ID of the user making the request.
* @return bool
*/
public static function can_create_data_request_for_user($user, $requester = null) {
$usercontext = \context_user::instance($user);
return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
}
/**
* Require that the current user can make a data request for the specified other user.
*
* @param int $user The user ID of the target user.
* @param int $requester The user ID of the user making the request.
* @return bool
*/
public static function require_can_create_data_request_for_user($user, $requester = null) {
$usercontext = \context_user::instance($user);
require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
return true;
}
/**
* Checks whether a user can download a data request.
*
@@ -729,8 +736,6 @@ class api {
* @return \tool_dataprivacy\purpose.
*/
public static function create_purpose(stdClass $record) {
self::check_can_manage_data_registry();
$purpose = new purpose(0, $record);
$purpose->create();
@@ -744,8 +749,6 @@ class api {
* @return \tool_dataprivacy\purpose.
*/
public static function update_purpose(stdClass $record) {
self::check_can_manage_data_registry();
if (!isset($record->sensitivedatareasons)) {
$record->sensitivedatareasons = '';
}
@@ -765,8 +768,6 @@ class api {
* @return bool
*/
public static function delete_purpose($id) {
self::check_can_manage_data_registry();
$purpose = new purpose($id);
if ($purpose->is_used()) {
throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
@@ -780,8 +781,6 @@ class api {
* @return \tool_dataprivacy\purpose[]
*/
public static function get_purposes() {
self::check_can_manage_data_registry();
return purpose::get_records([], 'name', 'ASC');
}
@@ -792,8 +791,6 @@ class api {
* @return \tool_dataprivacy\category.
*/
public static function create_category(stdClass $record) {
self::check_can_manage_data_registry();
$category = new category(0, $record);
$category->create();
@@ -807,8 +804,6 @@ class api {
* @return \tool_dataprivacy\category.
*/
public static function update_category(stdClass $record) {
self::check_can_manage_data_registry();
$category = new category($record->id);
$category->from_record($record);
@@ -824,8 +819,6 @@ class api {
* @return bool
*/
public static function delete_category($id) {
self::check_can_manage_data_registry();
$category = new category($id);
if ($category->is_used()) {
throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
@@ -839,8 +832,6 @@ class api {
* @return \tool_dataprivacy\category[]
*/
public static function get_categories() {
self::check_can_manage_data_registry();
return category::get_records([], 'name', 'ASC');
}
@@ -851,8 +842,6 @@ class api {
* @return \tool_dataprivacy\context_instance
*/
public static function set_context_instance($record) {
self::check_can_manage_data_registry($record->contextid);
if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
// Update.
$instance->from_record($record);
@@ -879,7 +868,6 @@ class api {
* @return null
*/
public static function unset_context_instance(context_instance $instance) {
self::check_can_manage_data_registry($instance->get('contextid'));
$instance->delete();
}
@@ -893,9 +881,6 @@ class api {
public static function set_contextlevel($record) {
global $DB;
// Only manager at system level can set this.
self::check_can_manage_data_registry();
if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
throw new \coding_exception('Only context system and context user can set a contextlevel ' .
'purpose and retention');
@@ -927,7 +912,6 @@ class api {
* @return category|false
*/
public static function get_effective_context_category(\context $context, $forcedvalue=false) {
self::check_can_manage_data_registry($context->id);
if (!data_registry::defaults_set()) {
return false;
}
@@ -942,8 +926,7 @@ class api {
* @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
* @return purpose|false
*/
public static function get_effective_context_purpose(\context $context, $forcedvalue=false) {
self::check_can_manage_data_registry($context->id);
public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
if (!data_registry::defaults_set()) {
return false;
}
@@ -959,7 +942,6 @@ class api {
* @return category|false
*/
public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
self::check_can_manage_data_registry(\context_system::instance()->id);
if (!data_registry::defaults_set()) {
return false;
}
@@ -975,7 +957,6 @@ class api {
* @return purpose|false
*/
public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
self::check_can_manage_data_registry(\context_system::instance()->id);
if (!data_registry::defaults_set()) {
return false;
}
@@ -983,38 +964,6 @@ class api {
return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
}
/**
* Creates an expired context record for the provided context id.
*
* @param int $contextid
* @return \tool_dataprivacy\expired_context
*/
public static function create_expired_context($contextid) {
self::check_can_manage_data_registry();
$record = (object)[
'contextid' => $contextid,
'status' => expired_context::STATUS_EXPIRED,
];
$expiredctx = new expired_context(0, $record);
$expiredctx->save();
return $expiredctx;
}
/**
* Deletes an expired context record.
*
* @param int $id The tool_dataprivacy_ctxexpire id.
* @return bool True on success.
*/
public static function delete_expired_context($id) {
self::check_can_manage_data_registry();
$expiredcontext = new expired_context($id);
return $expiredcontext->delete();
}
/**
* Updates the status of an expired context.
*
@@ -1023,8 +972,6 @@ class api {
* @return null
*/
public static function set_expired_context_status(expired_context $expiredctx, $status) {
self::check_can_manage_data_registry();
$expiredctx->set('status', $status);
$expiredctx->save();
}
@@ -1175,8 +1122,6 @@ class api {
public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
global $DB;
self::check_can_manage_data_registry();
// Get the class name associated with this context level.
$classname = context_helper::get_class_for_level($contextlevel);
list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
@@ -191,14 +191,14 @@ class data_registry {
* @param int|false $forcedvalue Use this value as if this was this context instance value.
* @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element
*/
public static function get_effective_context_value(\context $context, $element, $forcedvalue=false) {
public static function get_effective_context_value(\context $context, $element, $forcedvalue = false) {
if ($element !== 'purpose' && $element !== 'category') {
throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
}
$fieldname = $element . 'id';
if ($forcedvalue === false) {
if (empty($forcedvalue)) {
$instance = context_instance::get_record_by_contextid($context->id, false);
if (!$instance) {
@@ -217,20 +217,29 @@ class data_registry {
// The effective value varies depending on the context level.
if ($context->contextlevel == CONTEXT_USER) {
// Use the context level value as we don't allow people to set specific instances values.
return self::get_effective_contextlevel_value($context->contextlevel, $element);
} else {
// Check if we need to pass the plugin name of an activity.
$forplugin = '';
if ($context->contextlevel == CONTEXT_MODULE) {
list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
$forplugin = $cm->modname;
}
// Use the default context level value.
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
$context->contextlevel, false, false, $forplugin
);
return self::get_element_instance($element, $$fieldname);
return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
}
$parents = $context->get_parent_contexts(true);
foreach ($parents as $parent) {
if ($parent->contextlevel == CONTEXT_USER) {
// Use the context level value as we don't allow people to set specific instances values.
return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
}
}
// Check if we need to pass the plugin name of an activity.
$forplugin = '';
if ($context->contextlevel == CONTEXT_MODULE) {
list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
$forplugin = $cm->modname;
}
// Use the default context level value.
list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
$context->contextlevel, false, false, $forplugin
);
return self::get_element_instance($element, $$fieldname);
}
// Specific value for this context instance.
@@ -159,4 +159,51 @@ class expired_context extends \core\persistent {
return $DB->count_records_sql($sql, $params);
}
/**
* Create a new expired_context based on the context, and expiry_info object.
*
* @param \context $context
* @param expiry_info $info
* @return expired_context
*/
public static function create_from_expiry_info(\context $context, expiry_info $info) {
$record = (object) [
'contextid' => $context->id,
'status' => self::STATUS_EXPIRED,
];
$expiredcontext = new static(0, $record);
$expiredcontext->save();
return $expiredcontext;
}
/**
* Update the expired_context from an expiry_info object which relates to this context.
*
* @param expiry_info $info
* @return $this
*/
public function update_from_expiry_info(expiry_info $info) {
return $this;
}
/**
* Check whether this expired_context record is in a state ready for deletion to actually take place.
*
* @return bool
*/
public function can_process_deletion() {
return ($this->get('status') == self::STATUS_APPROVED);
}
/**
* Check whether this expired_context record has already been cleaned.
*
* @return bool
*/
public function is_complete() {
return ($this->get('status') == self::STATUS_CLEANED);
}
}
@@ -34,121 +34,680 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class expired_contexts_manager {
class expired_contexts_manager {
/**
* Number of deleted contexts for each scheduled task run.
*/
const DELETE_LIMIT = 200;
/**
* Returns the list of expired context instances.
*
* @return \stdClass[]
*/
abstract protected function get_expired_contexts();
/** @var progress_trace The log progress tracer */
protected $progresstracer = null;
/**
* Specify with context levels this expired contexts manager is deleting.
*
* @return int[]
*/
abstract protected function get_context_levels();
/** @var manager The privacy manager */
protected $manager = null;
/**
* Flag expired contexts as expired.
*
* @return int The number of contexts flagged as expired.
* @return int[] The number of contexts flagged as expired for courses, and users.
*/
public function flag_expired() {
public function flag_expired_contexts() {
if (!$this->check_requirements()) {
return 0;
return [0, 0];
}
$contexts = $this->get_expired_contexts();
foreach ($contexts as $context) {
api::create_expired_context($context->id);
// Clear old and stale records first.
static::clear_old_records();
$data = static::get_nested_expiry_info_for_courses();
$coursecount = 0;
foreach ($data as $expiryrecord) {
if ($this->update_from_expiry_info($expiryrecord)) {
$coursecount++;
}
}
return count($contexts);
$data = static::get_nested_expiry_info_for_user();
$usercount = 0;
foreach ($data as $expiryrecord) {
if ($this->update_from_expiry_info($expiryrecord)) {
$usercount++;
}
}
return [$coursecount, $usercount];
}
/**
* Clear old and stale records.
*/
protected static function clear_old_records() {
global $DB;
$sql = "SELECT dpctx.*
FROM {tool_dataprivacy_ctxexpired} dpctx
LEFT JOIN {context} ctx ON ctx.id = dpctx.contextid
WHERE ctx.id IS NULL";
$orphaned = $DB->get_recordset_sql($sql);
foreach ($orphaned as $orphan) {
$expiredcontext = new expired_context(0, $orphan);
$expiredcontext->delete();
}
// Delete any child of a user context.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
$params = [
'contextuser' => CONTEXT_USER,
];
$sql = "SELECT dpctx.*
FROM {tool_dataprivacy_ctxexpired} dpctx
WHERE dpctx.contextid IN (
SELECT ctx.id
FROM {context} ctxuser
JOIN {context} ctx ON ctx.path LIKE {$parentpath}
WHERE ctxuser.contextlevel = :contextuser
)";
$userchildren = $DB->get_recordset_sql($sql, $params);
foreach ($userchildren as $child) {
$expiredcontext = new expired_context(0, $child);
$expiredcontext->delete();
}
}
/**
* Get the full nested set of expiry data relating to all contexts.
*
* @param string $contextpath A contexpath to restrict results to
* @return \stdClass[]
*/
protected static function get_nested_expiry_info($contextpath = '') {
$coursepaths = self::get_nested_expiry_info_for_courses($contextpath);
$userpaths = self::get_nested_expiry_info_for_user($contextpath);
return array_merge($coursepaths, $userpaths);
}
/**
* Get the full nested set of expiry data relating to course-related contexts.
*
* @param string $contextpath A contexpath to restrict results to
* @return \stdClass[]
*/
protected static function get_nested_expiry_info_for_courses($contextpath = '') {
global $DB;
$contextfields = \context_helper::get_preload_record_columns_sql('ctx');
$expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');
$purposefields = 'dpctx.purposeid';
$coursefields = 'ctxcourse.expirydate AS expirydate';
$fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $coursefields, $purposefields]);
// We want all contexts at course-dependant levels.
$parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");
// This SQL query returns all course-dependant contexts (including the course context)
// which course end date already passed.
// This is ordered by the context path in reverse order, which will give the child nodes before any parent node.
$params = [
'contextlevel' => CONTEXT_COURSE,
];
$where = '';
if (!empty($contextpath)) {
$where = "WHERE (ctx.path = :pathmatchexact OR ctx.path LIKE :pathmatchchildren)";
$params['pathmatchexact'] = $contextpath;
$params['pathmatchchildren'] = "{$contextpath}/%";
}
$sql = "SELECT $fields
FROM {context} ctx
JOIN (
SELECT c.enddate AS expirydate, subctx.path
FROM {context} subctx
JOIN {course} c
ON subctx.contextlevel = :contextlevel
AND subctx.instanceid = c.id
AND c.format != 'site'
) ctxcourse
ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
ON dpctx.contextid = ctx.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
ON ctx.id = expiredctx.contextid
{$where}
ORDER BY ctx.path DESC";
return self::get_nested_expiry_info_from_sql($sql, $params);
}
/**
* Get the full nested set of expiry data.
*
* @param string $contextpath A contexpath to restrict results to
* @return \stdClass[]
*/
protected static function get_nested_expiry_info_for_user($contextpath = '') {
global $DB;
$contextfields = \context_helper::get_preload_record_columns_sql('ctx');
$expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');
$purposefields = 'dpctx.purposeid';
$userfields = 'u.lastaccess AS expirydate';
$fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $userfields, $purposefields]);
// We want all contexts at user-dependant levels.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
// This SQL query returns all user-dependant contexts (including the user context)
// This is ordered by the context path in reverse order, which will give the child nodes before any parent node.
$params = [
'contextlevel' => CONTEXT_USER,
];
$where = '';
if (!empty($contextpath)) {
$where = "AND ctx.path = :pathmatchexact";
$params['pathmatchexact'] = $contextpath;
}
$sql = "SELECT $fields, u.deleted AS userdeleted
FROM {context} ctx
JOIN {user} u ON ctx.instanceid = u.id
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
ON dpctx.contextid = ctx.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
ON ctx.id = expiredctx.contextid
WHERE ctx.contextlevel = :contextlevel {$where}
ORDER BY ctx.path DESC";
return self::get_nested_expiry_info_from_sql($sql, $params);
}
/**
* Get the full nested set of expiry data given appropriate SQL.
* Only contexts which have expired will be included.
*
* @param string $sql The SQL used to select the nested information.
* @param array $params The params required by the SQL.
* @return \stdClass[]
*/
protected static function get_nested_expiry_info_from_sql($sql, array $params) {
global $DB;
$fulllist = $DB->get_recordset_sql($sql, $params);
$datalist = [];
$expiredcontents = [];
$pathstoskip = [];
foreach ($fulllist as $record) {
\context_helper::preload_from_record($record);
$context = \context::instance_by_id($record->id, false);
if (!self::is_eligible_for_deletion($pathstoskip, $context)) {
// We should skip this context, and therefore all of it's children.
$datalist = array_filter($datalist, function($data, $path) use ($context) {
// Remove any child of this context.
// Technically this should never be fulfilled because the query is ordered in path DESC, but is kept
// in to be certain.
return (false === strpos($path, "{$context->path}/"));
}, ARRAY_FILTER_USE_BOTH);
if ($record->expiredctxid) {
// There was previously an expired context record.
// Delete it to be on the safe side.
$expiredcontext = new expired_context(null, expired_context::extract_record($record, 'expiredctx'));
$expiredcontext->delete();
}
continue;
}
$purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
$purpose = api::get_effective_context_purpose($context, $purposevalue);
if ($context instanceof \context_user && !empty($record->userdeleted)) {
$expiryinfo = static::get_expiry_info($purpose, $record->userdeleted);
} else {
$expiryinfo = static::get_expiry_info($purpose, $record->expirydate);
}
foreach ($datalist as $path => $data) {
// Merge with already-processed children.
if (strpos($path, $context->path) !== 0) {
continue;
}
$expiryinfo->merge_with_child($data->info);
}
$datalist[$context->path] = (object) [
'context' => $context,
'record' => $record,
'purpose' => $purpose,
'info' => $expiryinfo,
];
}
$fulllist->close();
return $datalist;
}
/**
* Check whether the supplied context would be elible for deletion.
*
* @param array $pathstoskip A set of paths which should be skipped
* @param \context $context
* @return bool
*/
protected static function is_eligible_for_deletion(array &$pathstoskip, \context $context) {
$shouldskip = false;
// Check whether any of the child contexts are ineligble.
$shouldskip = !empty(array_filter($pathstoskip, function($path) use ($context) {
// If any child context has already been skipped then it will appear in this list.
// Since paths include parents, test if the context under test appears as the haystack in the skipped
// context's needle.
return false !== (strpos($context->path, $path));
}));
if (!$shouldskip && $context instanceof \context_user) {
// The context instanceid is the user's ID.
if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) {
// This is an admin, or the guest and cannot be deleted.
$shouldskip = true;
}
if (!$shouldskip) {
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
$requireenddate = self::require_all_end_dates_for_user_deletion();
foreach ($courses as $course) {
if (empty($course->enddate)) {
// This course has no end date.
if ($requireenddate) {
// Course end dates are required, and this course has no end date.
$shouldskip = true;
break;
}
// Course end dates are not required. The subsequent checks are pointless at this time so just
// skip them.
continue;
}
if ($course->enddate >= time()) {
// This course is still in the future.
$shouldskip = true;
break;
}
// This course has an end date which is in the past.
if (!self::is_course_expired($course)) {
// This course has not expired yet.
$shouldskip = true;
break;
}
}
}
}
if ($shouldskip) {
// Add this to the list of contexts to skip for parentage checks.
$pathstoskip[] = $context->path;
}
return !$shouldskip;
}
/**
* Deletes the expired contexts.
*
* @return int The number of deleted contexts.
* @return int[] The number of deleted contexts.
*/
public function delete() {
$numprocessed = 0;
public function process_approved_deletions() {
if (!$this->check_requirements()) {
return $numprocessed;
return [0, 0];
}
$privacymanager = new manager();
$privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
$expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]);
$totalprocessed = 0;
$usercount = 0;
$coursecount = 0;
foreach ($expiredcontexts as $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
if (empty($context)) {
// Unable to process this request further.
// We have no context to delete.
$expiredctx->delete();
continue;
}
foreach ($this->get_context_levels() as $level) {
$expiredcontexts = expired_context::get_records_by_contextlevel($level, expired_context::STATUS_APPROVED);
foreach ($expiredcontexts as $expiredctx) {
if (!$this->delete_expired_context($privacymanager, $expiredctx)) {
continue;
if ($this->delete_expired_context($expiredctx)) {
if ($context instanceof \context_user) {
$usercount++;
} else {
$coursecount++;
}
$numprocessed += 1;
if ($numprocessed == self::DELETE_LIMIT) {
// Close the recordset.
$expiredcontexts->close();
break 2;
$totalprocessed++;
if ($totalprocessed >= $this->get_delete_limit()) {
break;
}
}
}
return $numprocessed;
return [$coursecount, $usercount];
}
/**
* Deletes user data from the provided context.
*
* @param manager $privacymanager
* @param expired_context $expiredctx
* @return \context|false
*/
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
protected function delete_expired_context(expired_context $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'));
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
if (!$context) {
api::delete_expired_context($expiredctx->get('contextid'));
$this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true));
// Update the expired_context and verify that it is still ready for deletion.
$expiredctx = $this->update_expired_context($expiredctx);
if (empty($expiredctx)) {
$this->get_progress()->output("Context has changed since approval and is no longer pending approval. Skipping", 1);
return false;
}
if (!PHPUNIT_TEST) {
mtrace('Deleting context ' . $context->id . ' - ' .
shorten_text($context->get_context_name(true, true)));
if (!$expiredctx->can_process_deletion()) {
// This only happens if the record was updated after being first fetched.
$this->get_progress()->output("Context has changed since approval and must be re-approved. Skipping", 1);
$expiredctx->set('status', expired_context::STATUS_EXPIRED);
$expiredctx->save();
return false;
}
$privacymanager->delete_data_for_all_users_in_context($context);
api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
$privacymanager = $this->get_privacy_manager();
if ($context instanceof \context_user) {
$this->delete_expired_user_context($expiredctx);
} else {
// This context is fully expired - that is that the default retention period has been reached.
$privacymanager->delete_data_for_all_users_in_context($context);
}
// Mark the record as cleaned.
$expiredctx->set('status', expired_context::STATUS_CLEANED);
$expiredctx->save();
return $context;
}
/**
* Deletes user data from the provided user context.
*
* @param expired_context $expiredctx
*/
protected function delete_expired_user_context(expired_context $expiredctx) {
global $DB;
$contextid = $expiredctx->get('contextid');
$context = \context::instance_by_id($contextid);
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
$privacymanager = $this->get_privacy_manager();
// Delete all child contexts of the user context.
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
$params = [
'contextlevel' => CONTEXT_USER,
'contextid' => $expiredctx->get('contextid'),
];
$fields = \context_helper::get_preload_record_columns_sql('ctx');
$sql = "SELECT ctx.id, $fields
FROM {context} ctxuser
JOIN {context} ctx ON ctx.path LIKE {$parentpath}
WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextid
ORDER BY ctx.path DESC";
$children = $DB->get_recordset_sql($sql, $params);
foreach ($children as $child) {
\context_helper::preload_from_record($child);
$context = \context::instance_by_id($child->id);
$privacymanager->delete_data_for_all_users_in_context($context);
}
$children->close();
// Delete all unprotected data that the user holds.
$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
foreach ($contextlistcollection as $contextlist) {
$contextids = [];
$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
$user,
$contextlist->get_component(),
$contextlist->get_contextids()
));
}
$privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress());
// Delete the user context.
$context = \context::instance_by_id($expiredctx->get('contextid'));
$privacymanager->delete_data_for_all_users_in_context($context);
// This user is now fully expired - finish by deleting the user.
delete_user($user);
}
/**
* Whether end dates are required on all courses in order for a user to be expired from them.
*
* @return bool
*/
protected static function require_all_end_dates_for_user_deletion() {
$requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion');
return !empty($requireenddate);
}
/**
* Check that the requirements to start deleting contexts are satisified.
*
* @return bool
*/
protected function check_requirements() {
api::check_can_manage_data_registry(\context_system::instance()->id);
if (!data_registry::defaults_set()) {
return false;
}
return true;
}
/**
* Check whether a date is beyond the specified period.
*
* @param string $period The Expiry Period
* @param int $comparisondate The date for comparison
* @return bool
*/
protected static function has_expired($period, $comparisondate) {
$dt = new \DateTime();
$dt->setTimestamp($comparisondate);
$dt->add(new \DateInterval($period));
return (time() >= $dt->getTimestamp());
}
/**
* Get the expiry info object for the specified purpose and comparison date.
*
* @param purpose $purpose The purpose of this context
* @param int $comparisondate The date for comparison
* @return expiry_info
*/
protected static function get_expiry_info(purpose $purpose, $comparisondate = 0) {
if (empty($comparisondate)) {
// The date is empty, therefore this context cannot be considered for automatic expiry.
$defaultexpired = false;
} else {
$defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
}
return new expiry_info($defaultexpired);
}
/**
* Update or delete the expired_context from the expiry_info object.
* This function depends upon the data structure returned from get_nested_expiry_info.
*
* If the context is expired in any way, then an expired_context will be returned, otherwise null will be returned.
*
* @param \stdClass $expiryrecord
* @return expired_context|null
*/
protected function update_from_expiry_info(\stdClass $expiryrecord) {
if ($expiryrecord->info->is_any_expired()) {
// The context is expired in some fashion.
// Create or update as required.
if ($expiryrecord->record->expiredctxid) {
$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
$expiredcontext->update_from_expiry_info($expiryrecord->info);
if ($expiredcontext->is_complete()) {
return null;
}
} else {
$expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);
}
return $expiredcontext;
} else {
// The context is not expired.
if ($expiryrecord->record->expiredctxid) {
// There was previously an expired context record, but it is no longer relevant.
// Delete it to be on the safe side.
$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
$expiredcontext->delete();
}
return null;
}
}
/**
* Update the expired context record.
*
* Note: You should use the return value as the provided value will be used to fetch data only.
*
* @param expired_context $expiredctx The record to update
* @return expired_context|null
*/
protected function update_expired_context(expired_context $expiredctx) {
// Fetch the context from the expired_context record.
$context = \context::instance_by_id($expiredctx->get('contextid'));
// Fetch the current nested expiry data.
$expiryrecords = self::get_nested_expiry_info($context->path);
// Find the current record.
if (empty($expiryrecords[$context->path])) {
$expiredctx->delete();
return null;
}
// Refresh the record.
// Note: Use the returned expiredctx.
$expiredctx = $this->update_from_expiry_info($expiryrecords[$context->path]);
if (empty($expiredctx)) {
return null;
}
if (!$context instanceof \context_user) {
// Where the target context is not a user, we check all children of the context.
// The expiryrecords array only contains children, fetched from the get_nested_expiry_info call above.
// No need to check that these _are_ children.
foreach ($expiryrecords as $expiryrecord) {
if ($expiryrecord->context->id === $context->id) {
// This is record for the context being tested that we checked earlier.
continue;
}
if (empty($expiryrecord->record->expiredctxid)) {
// There is no expired context record for this context.
// If there is no record, then this context cannot have been approved for removal.
return null;
}
// Fetch the expired_context object for this record.
// This needs to be updated from the expiry_info data too as there may be child changes to consider.
$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));
$expiredcontext->update_from_expiry_info($expiryrecord->info);
if (!$expiredcontext->is_complete()) {
return null;
}
}
}
return $expiredctx;
}
/**
* Check whether the course has expired.
*
* @param \stdClass $course
* @return bool
*/
protected static function is_course_expired(\stdClass $course) {
$context = \context_course::instance($course->id);
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired();
}
/**
* Create a new instance of the privacy manager.
*
* @return manager
*/
protected function get_privacy_manager() {
if (null === $this->manager) {
$this->manager = new manager();
$this->manager->set_observer(new \tool_dataprivacy\manager_observer());
}
return $this->manager;
}
/**
* Fetch the limit for the maximum number of contexts to delete in one session.
*
* @return int
*/
protected function get_delete_limit() {
return self::DELETE_LIMIT;
}
/**
* Get the progress tracer.
*
* @return \progress_trace
*/
protected function get_progress() {
if (null === $this->progresstracer) {
$this->set_progress(new \text_progress_trace());
}
return $this->progresstracer;
}
/**
* Set a specific tracer for the task.
*
* @param \progress_trace $trace
* @return $this
*/
public function set_progress(\progress_trace $trace) {
$this->progresstracer = $trace;
return $this;
}
}
@@ -1,135 +0,0 @@
<?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/>.
/**
* Expired contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK.
*
* @package tool_dataprivacy
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use tool_dataprivacy\purpose;
defined('MOODLE_INTERNAL') || die();
/**
* Expired contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class expired_course_related_contexts extends \tool_dataprivacy\expired_contexts_manager {
/**
* Course-related context levels.
*
* @return int[]
*/
protected function get_context_levels() {
return [CONTEXT_MODULE, CONTEXT_BLOCK, CONTEXT_COURSE];
}
/**
* Returns a recordset with user context instances that are possibly expired (to be confirmed by get_recordset_callback).
*
* @return \stdClass[]
*/
protected function get_expired_contexts() {
global $DB;
// Including context info + course end date + purposeid (this last one only if defined).
$fields = 'ctx.id AS id, ctxcourse.enddate AS courseenddate, dpctx.purposeid AS purposeid, ' .
\context_helper::get_preload_record_columns_sql('ctx');
// We want all contexts at course-dependant levels.
$parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");
// This SQL query returns all course-dependant contexts (including the course context)
// which course end date already passed.
$sql = "SELECT $fields
FROM {context} ctx
JOIN (
SELECT c.enddate, subctx.path
FROM {context} subctx
JOIN {course} c
ON subctx.contextlevel = ? AND subctx.instanceid = c.id
WHERE c.enddate < ? AND c.enddate > 0
) ctxcourse
ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path
LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx
ON dpctx.contextid = ctx.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx
ON ctx.id = expiredctx.contextid
WHERE expiredctx.id IS NULL
ORDER BY ctx.contextlevel DESC, ctx.path";
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_COURSE, time()]);
$expiredcontexts = [];
$excludedcontextids = [];
foreach ($possiblyexpired as $record) {
\context_helper::preload_from_record($record);
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
// module contexts below it will not exist).
$context = \context::instance_by_id($record->id, false);
if (!$context) {
continue;
}
// We pass the value we just got from SQL so get_effective_context_purpose don't need to query
// the db again to retrieve it. If there is no tool_dataprovider_ctxinstance record
// $record->purposeid will be null which is ok as it would force get_effective_context_purpose
// to return the default purpose for the context context level (no db queries involved).
$purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
// It should be cheap as system purposes and context level purposes will be retrieved from a cache most of the time.
$purpose = api::get_effective_context_purpose($context, $purposevalue);
$dt = new \DateTime();
$dt->setTimestamp($record->courseenddate);
$di = new \DateInterval($purpose->get('retentionperiod'));
$dt->add($di);
if (time() < $dt->getTimestamp()) {
// Exclude this context ID as it has not reached the retention period yet.
$excludedcontextids[] = $context->id;
continue;
}
// Check if this context has children that have not yet expired.
$hasunexpiredchildren = false;
$children = $context->get_child_contexts();
foreach ($children as $child) {
if (in_array($child->id, $excludedcontextids)) {
$hasunexpiredchildren = true;
break;
}
}
if ($hasunexpiredchildren) {
// Exclude this context ID as it has children that have not yet expired.
$excludedcontextids[] = $context->id;
continue;
}
$expiredcontexts[$context->id] = $context;
}
return $expiredcontexts;
}
}
@@ -1,150 +0,0 @@
<?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/>.
/**
* Expired contexts manager for CONTEXT_USER.
*
* @package tool_dataprivacy
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use core_privacy\manager;
defined('MOODLE_INTERNAL') || die();
/**
* Expired contexts manager for CONTEXT_USER.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
/**
* Only user level.
*
* @return int[]
*/
protected function get_context_levels() {
return [CONTEXT_USER];
}
/**
* Returns the user context instances that are expired.
*
* @return \stdClass[]
*/
protected function get_expired_contexts() {
global $DB;
// Including context info + last login timestamp.
$fields = 'ctx.id AS id, ' . \context_helper::get_preload_record_columns_sql('ctx');
$purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
// Calculate what is considered expired according to the context level effective purpose (= now + retention period).
$expiredtime = new \DateTime();
$retention = new \DateInterval($purpose->get('retentionperiod'));
$expiredtime->sub($retention);
$sql = "SELECT $fields FROM {context} ctx
JOIN {user} u ON ctx.contextlevel = ? AND ctx.instanceid = u.id
LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid
WHERE u.lastaccess <= ? AND u.lastaccess > 0 AND expiredctx.id IS NULL
ORDER BY ctx.path, ctx.contextlevel ASC";
$possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_USER, $expiredtime->getTimestamp()]);
$expiredcontexts = [];
foreach ($possiblyexpired as $record) {
\context_helper::preload_from_record($record);
// No strict checking as the context may already be deleted (e.g. we just deleted a course,
// module contexts below it will not exist).
$context = \context::instance_by_id($record->id, false);
if (!$context) {
continue;
}
if (is_siteadmin($context->instanceid)) {
continue;
}
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
foreach ($courses as $course) {
if (!$course->enddate) {
// We can not know it what is going on here, so we prefer to be conservative.
continue 2;
}
if ($course->enddate >= time()) {
// Future or ongoing course.
continue 2;
}
}
$expiredcontexts[$context->id] = $context;
}
return $expiredcontexts;
}
/**
* Deletes user data from the provided context.
*
* Overwritten to delete the user.
*
* @param manager $privacymanager
* @param expired_context $expiredctx
* @return \context|false
*/
protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
if (!$context) {
api::delete_expired_context($expiredctx->get('contextid'));
return false;
}
if (!PHPUNIT_TEST) {
mtrace('Deleting context ' . $context->id . ' - ' .
shorten_text($context->get_context_name(true, true)));
}
// To ensure that all user data is deleted, instead of deleting by context, we run through and collect any stray
// contexts for the user that may still exist and call delete_data_for_user().
$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
foreach ($contextlistcollection as $contextlist) {
$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
$user,
$contextlist->get_component(),
$contextlist->get_contextids()
));
}
$privacymanager->delete_data_for_user($approvedlistcollection);
api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
// Delete the user.
delete_user($user);
return $context;
}
}
@@ -0,0 +1,93 @@
<?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/>.
/**
* Expiry Data.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use core_privacy\manager;
defined('MOODLE_INTERNAL') || die();
/**
* Expiry Data.
*
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class expiry_info {
/** @var bool Whether this context is fully expired */
protected $isexpired = false;
/**
* Constructor for the expiry_info class.
*
* @param bool $isexpired Whether the retention period for this context has expired yet.
*/
public function __construct($isexpired) {
$this->isexpired = $isexpired;
}
/**
* Whether this context has 'fully' expired.
* That is to say that the default retention period has been reached, and that there are no unexpired roles.
*
* @return bool
*/
public function is_fully_expired() {
return $this->isexpired;
}
/**
* Whether any part of this context has expired.
*
* @return bool
*/
public function is_any_expired() {
if ($this->is_fully_expired()) {
return true;
}
return false;
}
/**
* Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
*
* It is not possible to delete any part of a context that is not deleted by a parent.
* So if a course's retention policy has been reached, then only parts where the children have also expired can be
* deleted.
*
* @param expiry_info $child The child record to merge with.
* @return $this
*/
public function merge_with_child(expiry_info $child) {
if ($child->is_fully_expired()) {
return $this;
}
// If the child is not fully expired, then none of the parents can be either.
$this->isexpired = false;
return $this;
}
}
+60 -27
View File
@@ -90,17 +90,30 @@ class external extends external_api {
]);
$requestid = $params['requestid'];
// Validate context.
// Validate context and access to manage the registry.
$context = context_user::instance($USER->id);
self::validate_context($context);
// Ensure the request exists.
$select = 'id = :id AND (userid = :userid OR requestedby = :requestedby)';
$params = ['id' => $requestid, 'userid' => $USER->id, 'requestedby' => $USER->id];
$requestexists = data_request::record_exists_select($select, $params);
$requests = data_request::get_records_select($select, $params);
$requestexists = count($requests) === 1;
$result = false;
if ($requestexists) {
$request = reset($requests);
$datasubject = $request->get('userid');
if ($datasubject !== $USER->id) {
// The user is not the subject. Check that they can cancel this request.
if (!api::can_create_data_request_for_user($datasubject)) {
$forusercontext = \context_user::instance($datasubject);
throw new required_capability_exception($forusercontext,
'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
}
}
// TODO: Do we want a request to be non-cancellable past a certain point? E.g. When it's already approved/processing.
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
} else {
@@ -249,9 +262,10 @@ class external extends external_api {
]);
$requestid = $params['requestid'];
// Validate context.
// Validate context and access to manage the registry.
$context = context_system::instance();
self::validate_context($context);
api::check_can_manage_data_registry();
$message = get_string('markedcomplete', 'tool_dataprivacy');
// Update the data request record.
@@ -725,7 +739,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -790,6 +806,10 @@ class external extends external_api {
'id' => $id
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$result = api::delete_purpose($params['id']);
return [
@@ -836,7 +856,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -901,6 +923,10 @@ class external extends external_api {
'id' => $id
]);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$result = api::delete_category($params['id']);
return [
@@ -947,8 +973,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Extra permission checkings are delegated to api::set_contextlevel.
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -962,7 +989,6 @@ class external extends external_api {
$contextlevel = api::set_contextlevel($validateddata);
} else if ($errors = $mform->is_validated()) {
$warnings[] = json_encode($errors);
throw new moodle_exception('generalerror');
}
if ($contextlevel) {
@@ -1014,8 +1040,9 @@ class external extends external_api {
'jsonformdata' => $jsonformdata
]);
// Extra permission checkings are delegated to api::set_context_instance.
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$serialiseddata = json_decode($params['jsonformdata']);
$data = array();
@@ -1025,6 +1052,7 @@ class external extends external_api {
$customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context);
$mform = new \tool_dataprivacy\form\context_instance(null, $customdata, 'post', '', null, true, $data);
if ($validateddata = $mform->get_data()) {
api::check_can_manage_data_registry($validateddata->contextid);
$context = api::set_context_instance($validateddata);
} else if ($errors = $mform->is_validated()) {
$warnings[] = json_encode($errors);
@@ -1148,9 +1176,9 @@ class external extends external_api {
]);
$ids = $params['ids'];
// Validate context.
$context = context_system::instance();
self::validate_context($context);
// Validate context and access to manage the registry.
self::validate_context(\context_system::instance());
api::check_can_manage_data_registry();
$result = true;
if (!empty($ids)) {
@@ -1160,24 +1188,27 @@ class external extends external_api {
$expiredcontext = new expired_context($id);
$targetcontext = context_helper::instance_by_id($expiredcontext->get('contextid'));
// Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
$childcontexts = $targetcontext->get_child_contexts();
foreach ($childcontexts as $child) {
if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
// Add this child context to the list for approval.
$expiredcontextstoapprove[] = $expiredchildcontext;
} else {
// This context has not yet been flagged for deletion.
$result = false;
$message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
$targetcontext->get_context_name(false));
$warnings[] = [
'item' => 'tool_dataprivacy_ctxexpired',
'warningcode' => 'errorcontexthasunexpiredchildren',
'message' => $message
];
// Exit the process.
break 2;
if (!$targetcontext instanceof \context_user) {
// Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
// User context children do not need to be considered.
$childcontexts = $targetcontext->get_child_contexts();
foreach ($childcontexts as $child) {
if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
// Add this child context to the list for approval.
$expiredcontextstoapprove[] = $expiredchildcontext;
} else {
// This context has not yet been flagged for deletion.
$result = false;
$message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
$targetcontext->get_context_name(false));
$warnings[] = [
'item' => 'tool_dataprivacy_ctxexpired',
'warningcode' => 'errorcontexthasunexpiredchildren',
'message' => $message
];
// Exit the process.
break 2;
}
}
}
@@ -1268,6 +1299,7 @@ class external extends external_api {
// Validate context.
$context = context_system::instance();
self::validate_context($context);
api::check_can_manage_data_registry();
// Set the context defaults.
$result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
@@ -1321,6 +1353,7 @@ class external extends external_api {
$context = context_system::instance();
self::validate_context($context);
api::check_can_manage_data_registry();
$categories = api::get_categories();
$options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
@@ -52,18 +52,10 @@ class delete_expired_contexts extends scheduled_task {
/**
* Run the task to delete context instances based on their retention periods.
*
*/
public function execute() {
$manager = new \tool_dataprivacy\expired_course_related_contexts();
$deleted = $manager->delete();
if ($deleted > 0) {
mtrace($deleted . ' course-related contexts have been deleted');
}
$manager = new \tool_dataprivacy\expired_user_contexts();
$deleted = $manager->delete();
if ($deleted > 0) {
mtrace($deleted . ' user contexts have been deleted');
}
$manager = new \tool_dataprivacy\expired_contexts_manager();
list($courses, $users) = $manager->process_approved_deletions();
mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired");
}
}
@@ -54,15 +54,8 @@ class expired_retention_period extends scheduled_task {
* Run the task to flag context instances as expired.
*/
public function execute() {
$manager = new \tool_dataprivacy\expired_course_related_contexts();
$flagged = $manager->flag_expired();
if ($flagged > 0) {
mtrace($flagged . ' course-related contexts have been flagged as expired');
}
$manager = new \tool_dataprivacy\expired_user_contexts();
$flagged = $manager->flag_expired();
if ($flagged > 0) {
mtrace($flagged . ' user contexts have been flagged as expired');
}
$manager = new \tool_dataprivacy\expired_contexts_manager();
list($courses, $users) = $manager->flag_expired_contexts();
mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired");
}
}
+8 -1
View File
@@ -24,7 +24,6 @@
require_once('../../../config.php');
require_once('lib.php');
require_once('classes/api.php');
require_once('createdatarequest_form.php');
$manage = optional_param('manage', 0, PARAM_INT);
@@ -67,6 +66,14 @@ if ($mform->is_cancelled()) {
// Data request submitted.
if ($data = $mform->get_data()) {
if ($data->userid != $USER->id) {
if (!\tool_dataprivacy\api::can_manage_data_requests($USER->id)) {
// If not a DPO, only users with the capability to make data requests for the user should be allowed.
// (e.g. users with the Parent role, etc).
\tool_dataprivacy\api::require_can_create_data_request_for_user($data->userid);
}
}
\tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments);
if ($manage) {
@@ -246,6 +246,15 @@ $string['requesttypeexport'] = 'Export all of my personal data';
$string['requesttypeexportshort'] = 'Export';
$string['requesttypeothers'] = 'General inquiry';
$string['requesttypeothersshort'] = 'Message';
$string['requireallenddatesforuserdeletion'] = 'Consider courses without end date as active';
$string['requireallenddatesforuserdeletion_desc'] = 'When calculating user expiry, several factors are considered:
* the user\'s last login time is compared against the retention period for users; and
* whether the user is actively enrolled in any courses.
When checking the active enrolment of a corse, if the course has no end date then this setting is used to determine whether that course is considered active or not.
If the course has no end date, and this setting is enabled, then the user cannot be deleted.';
$string['requiresattention'] = 'Requires attention.';
$string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
$string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.';
+6
View File
@@ -60,6 +60,12 @@ if ($hassiteconfig) {
new lang_string('dporolemapping_desc', 'tool_dataprivacy'), null, $roles)
);
}
// When calculating user expiry, should courses which have no end date be considered.
$privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/requireallenddatesforuserdeletion',
new lang_string('requireallenddatesforuserdeletion', 'tool_dataprivacy'),
new lang_string('requireallenddatesforuserdeletion_desc', 'tool_dataprivacy'),
1));
}
}
+242 -226
View File
@@ -46,16 +46,65 @@ global $CFG;
class tool_dataprivacy_api_testcase extends advanced_testcase {
/**
* setUp.
* Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
* tested with the default context.
*/
public function setUp() {
public function test_check_can_manage_data_registry_admin() {
$this->resetAfterTest();
$this->setAdminUser();
// Technically this actually returns void, but assertNull will suffice to avoid a pointless test.
$this->assertNull(api::check_can_manage_data_registry());
}
/**
* Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
* tested with the default context.
*/
public function test_check_can_manage_data_registry_without_cap_default() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
api::check_can_manage_data_registry();
}
/**
* Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
* tested with the default context.
*/
public function test_check_can_manage_data_registry_without_cap_system() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
api::check_can_manage_data_registry(\context_system::instance()->id);
}
/**
* Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is
* tested with the default context.
*/
public function test_check_can_manage_data_registry_without_cap_own_user() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
api::check_can_manage_data_registry(\context_user::instance($user->id)->id);
}
/**
* Test for api::update_request_status().
*/
public function test_update_request_status() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$s1 = $generator->create_user();
$this->setUser($s1);
@@ -65,6 +114,26 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$requestid = $datarequest->get('id');
// Update with a comment.
$comment = 'This is an example of a comment';
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $comment);
$this->assertTrue($result);
$datarequest = new data_request($requestid);
$this->assertStringEndsWith($comment, $datarequest->get('dpocomment'));
// Update with a comment which will be trimmed.
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, ' ');
$this->assertTrue($result);
$datarequest = new data_request($requestid);
$this->assertStringEndsWith($comment, $datarequest->get('dpocomment'));
// Update with a comment.
$secondcomment = ' - More comments - ';
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $secondcomment);
$this->assertTrue($result);
$datarequest = new data_request($requestid);
$this->assertRegExp("/.*{$comment}.*{$secondcomment}/s", $datarequest->get('dpocomment'));
// Update with a valid status.
$result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY);
$this->assertTrue($result);
@@ -82,6 +151,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* Test for api::get_site_dpos() when there are no users with the DPO role.
*/
public function test_get_site_dpos_no_dpos() {
$this->resetAfterTest();
$admin = get_admin();
$dpos = api::get_site_dpos();
@@ -95,6 +166,9 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
*/
public function test_get_site_dpos() {
global $DB;
$this->resetAfterTest();
$generator = new testing_data_generator();
$u1 = $generator->create_user();
$u2 = $generator->create_user();
@@ -130,6 +204,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_get_assigned_privacy_officer_roles() {
global $DB;
$this->resetAfterTest();
// Erroneously set the manager roles as the PO, even if it doesn't have the managedatarequests capability yet.
$managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
set_config('dporoles', $managerroleid, 'tool_dataprivacy');
@@ -171,6 +247,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_approve_data_request() {
global $DB;
$this->resetAfterTest();
$generator = new testing_data_generator();
$s1 = $generator->create_user();
$u1 = $generator->create_user();
@@ -213,6 +291,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_approve_data_request_not_yet_ready() {
global $DB;
$this->resetAfterTest();
$generator = new testing_data_generator();
$s1 = $generator->create_user();
$u1 = $generator->create_user();
@@ -243,6 +323,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* Test for api::approve_data_request() when called by a user who doesn't have the DPO role.
*/
public function test_approve_data_request_non_dpo_user() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$student = $generator->create_user();
$teacher = $generator->create_user();
@@ -252,17 +334,14 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
// Login as a user without DPO role.
$this->setUser($teacher);
$this->expectException(required_capability_exception::class);
api::approve_data_request($requestid);
}
/**
* Test for api::can_contact_dpo()
*/
public function test_can_contact_dpo() {
$this->resetAfterTest();
// Default ('contactdataprotectionofficer' is disabled by default).
$this->assertFalse(api::can_contact_dpo());
@@ -281,6 +360,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_can_manage_data_requests() {
global $DB;
$this->resetAfterTest();
// No configured site DPOs yet.
$admin = get_admin();
$this->assertTrue(api::can_manage_data_requests($admin->id));
@@ -317,10 +398,120 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
}
/**
* Test that a user who has no capability to make any data requests for children cannot create data requests for any
* other user.
*/
public function test_can_create_data_request_for_user_no() {
$this->resetAfterTest();
$parent = $this->getDataGenerator()->create_user();
$otheruser = $this->getDataGenerator()->create_user();
$this->setUser($parent);
$this->assertFalse(api::can_create_data_request_for_user($otheruser->id));
}
/**
* Test that a user who has the capability to make any data requests for one other user cannot create data requests
* for any other user.
*/
public function test_can_create_data_request_for_user_some() {
$this->resetAfterTest();
$parent = $this->getDataGenerator()->create_user();
$child = $this->getDataGenerator()->create_user();
$otheruser = $this->getDataGenerator()->create_user();
$systemcontext = \context_system::instance();
$parentrole = $this->getDataGenerator()->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $parent->id, \context_user::instance($child->id));
$this->setUser($parent);
$this->assertFalse(api::can_create_data_request_for_user($otheruser->id));
}
/**
* Test that a user who has the capability to make any data requests for one other user cannot create data requests
* for any other user.
*/
public function test_can_create_data_request_for_user_own_child() {
$this->resetAfterTest();
$parent = $this->getDataGenerator()->create_user();
$child = $this->getDataGenerator()->create_user();
$systemcontext = \context_system::instance();
$parentrole = $this->getDataGenerator()->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $parent->id, \context_user::instance($child->id));
$this->setUser($parent);
$this->assertTrue(api::can_create_data_request_for_user($child->id));
}
/**
* Test that a user who has no capability to make any data requests for children cannot create data requests for any
* other user.
*/
public function test_require_can_create_data_request_for_user_no() {
$this->resetAfterTest();
$parent = $this->getDataGenerator()->create_user();
$otheruser = $this->getDataGenerator()->create_user();
$this->setUser($parent);
$this->expectException('required_capability_exception');
api::require_can_create_data_request_for_user($otheruser->id);
}
/**
* Test that a user who has the capability to make any data requests for one other user cannot create data requests
* for any other user.
*/
public function test_require_can_create_data_request_for_user_some() {
$this->resetAfterTest();
$parent = $this->getDataGenerator()->create_user();
$child = $this->getDataGenerator()->create_user();
$otheruser = $this->getDataGenerator()->create_user();
$systemcontext = \context_system::instance();
$parentrole = $this->getDataGenerator()->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $parent->id, \context_user::instance($child->id));
$this->setUser($parent);
$this->expectException('required_capability_exception');
api::require_can_create_data_request_for_user($otheruser->id);
}
/**
* Test that a user who has the capability to make any data requests for one other user cannot create data requests
* for any other user.
*/
public function test_require_can_create_data_request_for_user_own_child() {
$this->resetAfterTest();
$parent = $this->getDataGenerator()->create_user();
$child = $this->getDataGenerator()->create_user();
$systemcontext = \context_system::instance();
$parentrole = $this->getDataGenerator()->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $parent->id, \context_user::instance($child->id));
$this->setUser($parent);
$this->assertTrue(api::require_can_create_data_request_for_user($child->id));
}
/**
* Test for api::can_download_data_request_for_user()
*/
public function test_can_download_data_request_for_user() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Three victims.
@@ -372,6 +563,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* Test for api::create_data_request()
*/
public function test_create_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$comment = 'sample comment';
@@ -399,6 +592,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_create_data_request_by_dpo() {
global $USER;
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$comment = 'sample comment';
@@ -426,6 +621,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_create_data_request_by_parent() {
global $DB;
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$parent = $generator->create_user();
@@ -461,6 +658,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* Test for api::deny_data_request()
*/
public function test_deny_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$comment = 'sample comment';
@@ -482,27 +681,6 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertTrue($result);
}
/**
* Test for api::deny_data_request()
*/
public function test_deny_data_request_without_permissions() {
$generator = new testing_data_generator();
$user = $generator->create_user();
$comment = 'sample comment';
// Login as user.
$this->setUser($user->id);
// Test data request creation.
$datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as a non-DPO user and try to call deny_data_request.
$user2 = $generator->create_user();
$this->setUser($user2);
$this->expectException(required_capability_exception::class);
api::deny_data_request($datarequest->get('id'));
}
/**
* Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests().
*
@@ -535,6 +713,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* @param int[] $statuses Status filters.
*/
public function test_get_data_requests($usertype, $fetchall, $statuses) {
$this->resetAfterTest();
$generator = new testing_data_generator();
$user1 = $generator->create_user();
$user2 = $generator->create_user();
@@ -667,6 +847,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* @param bool $expected The expected result.
*/
public function test_has_ongoing_request($status, $expected) {
$this->resetAfterTest();
$generator = new testing_data_generator();
$user1 = $generator->create_user();
@@ -700,6 +882,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_is_site_dpo() {
global $DB;
$this->resetAfterTest();
// No configured site DPOs yet.
$admin = get_admin();
$this->assertTrue(api::is_site_dpo($admin->id));
@@ -750,6 +934,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* @param string $comments The requestor's message to the DPO.
*/
public function test_notify_dpo($byadmin, $type, $typestringid, $comments) {
$this->resetAfterTest();
$generator = new testing_data_generator();
$user1 = $generator->create_user();
// Let's just use admin as DPO (It's the default if not set).
@@ -784,86 +970,13 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertContains(fullname($user1), $message->fullmessage);
}
/**
* Test of creating purpose as a user without privileges.
*/
public function test_create_purpose_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
api::create_purpose((object)[
'name' => 'aaa',
'description' => '<b>yeah</b>',
'descriptionformat' => 1,
'retentionperiod' => 'PT1M'
]);
}
/**
* Test fetching of purposes as a user without privileges.
*/
public function test_get_purposes_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setAdminUser();
api::create_purpose((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1,
'retentionperiod' => 'PT1M',
'lawfulbases' => 'gdpr_art_6_1_a'
]);
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
api::get_purposes();
}
/**
* Test updating of purpose as a user without privileges.
*/
public function test_update_purposes_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setAdminUser();
$purpose = api::create_purpose((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1,
'retentionperiod' => 'PT1M',
'lawfulbases' => 'gdpr_art_6_1_a'
]);
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
$purpose->set('retentionperiod', 'PT2M');
api::update_purpose($purpose->to_record());
}
/**
* Test purpose deletion as a user without privileges.
*/
public function test_delete_purpose_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setAdminUser();
$purpose = api::create_purpose((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1,
'retentionperiod' => 'PT1M',
'lawfulbases' => 'gdpr_art_6_1_a'
]);
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
api::delete_purpose($purpose->get('id'));
}
/**
* Test data purposes CRUD actions.
*
* @return null
*/
public function test_purpose_crud() {
$this->resetAfterTest();
$this->setAdminUser();
@@ -899,86 +1012,13 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertCount(0, api::get_purposes());
}
/**
* Test creation of data categories as a user without privileges.
*/
public function test_create_category_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
api::create_category((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1
]);
}
/**
* Test fetching of data categories as a user without privileges.
*/
public function test_get_categories_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setAdminUser();
api::create_category((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1
]);
// Back to a regular user.
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
api::get_categories();
}
/**
* Test updating of data category as a user without privileges.
*/
public function test_update_category_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setAdminUser();
$category = api::create_category((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1
]);
// Back to a regular user.
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
$category->set('name', 'yeah');
api::update_category($category->to_record());
}
/**
* Test deletion of data category as a user without privileges.
*/
public function test_delete_category_non_dpo_user() {
$pleb = $this->getDataGenerator()->create_user();
$this->setAdminUser();
$category = api::create_category((object)[
'name' => 'bbb',
'description' => '<b>yeah</b>',
'descriptionformat' => 1
]);
// Back to a regular user.
$this->setUser($pleb);
$this->expectException(required_capability_exception::class);
api::delete_category($category->get('id'));
$this->fail('Users shouldn\'t be allowed to manage categories by default');
}
/**
* Test data categories CRUD actions.
*
* @return null
*/
public function test_category_crud() {
$this->resetAfterTest();
$this->setAdminUser();
@@ -1018,6 +1058,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_context_instances() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
@@ -1052,6 +1094,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_contextlevel() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
@@ -1086,6 +1130,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
public function test_effective_contextlevel_defaults() {
$this->setAdminUser();
$this->resetAfterTest();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM);
@@ -1129,14 +1175,22 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertEquals($categories[0]->get('id'), $categoryid);
}
public function test_get_effective_contextlevel_category() {
// Before setup, get_effective_contextlevel_purpose will return false.
$this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_SYSTEM));
}
/**
* Test effective contextlevel return.
*
* @return null
*/
public function test_effective_contextlevel() {
$this->setAdminUser();
$this->resetAfterTest();
// Before setup, get_effective_contextlevel_purpose will return false.
$this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM));
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
// Set the system context level to purpose 1.
@@ -1184,6 +1238,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* @return null
*/
public function test_effective_context() {
$this->resetAfterTest();
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
@@ -1264,59 +1320,13 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertEquals($categories[1]->get('id'), $category->get('id'));
}
/**
* Tests the deletion of expired contexts.
*
* @return null
*/
public function test_expired_context_deletion() {
global $DB;
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
$course0context = \context_course::instance($courses[0]->id);
$course1context = \context_course::instance($courses[1]->id);
$expiredcontext0 = api::create_expired_context($course0context->id);
$this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired'));
$expiredcontext1 = api::create_expired_context($course1context->id);
$this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired'));
api::delete_expired_context($expiredcontext0->get('id'));
$this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired'));
}
/**
* Tests the status of expired contexts.
*
* @return null
*/
public function test_expired_context_status() {
global $DB;
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
$course0context = \context_course::instance($courses[0]->id);
$expiredcontext = api::create_expired_context($course0context->id);
// Default status.
$this->assertEquals(expired_context::STATUS_EXPIRED, $expiredcontext->get('status'));
api::set_expired_context_status($expiredcontext, expired_context::STATUS_APPROVED);
$this->assertEquals(expired_context::STATUS_APPROVED, $expiredcontext->get('status'));
}
/**
* Creates test purposes and categories.
*
* @return null
*/
protected function add_purposes_and_categories() {
$this->resetAfterTest();
$purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
$purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_b']);
@@ -1344,6 +1354,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* Test that delete requests filter out protected purpose contexts.
*/
public function test_add_request_contexts_with_status_delete() {
$this->resetAfterTest();
$data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_DELETE);
$contextids = $data->list->get_contextids();
@@ -1355,6 +1367,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* Test that export requests don't filter out protected purpose contexts.
*/
public function test_add_request_contexts_with_status_export() {
$this->resetAfterTest();
$data = $this->setup_test_add_request_contexts_with_status(api::DATAREQUEST_TYPE_EXPORT);
$contextids = $data->list->get_contextids();
@@ -1404,7 +1418,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* @param bool $override Whether to override instances.
*/
public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) {
$this->setAdminUser();
$this->resetAfterTest();
$generator = $this->getDataGenerator();
@@ -1527,6 +1541,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
* @return \stdClass
*/
protected function setup_test_add_request_contexts_with_status($type) {
$this->resetAfterTest();
$this->setAdminUser();
// User under test.
File diff suppressed because it is too large Load Diff
+418 -112
View File
@@ -40,71 +40,85 @@ use tool_dataprivacy\external;
*/
class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
/** @var stdClass The user making the request. */
protected $requester;
/** @var int The data request ID. */
protected $requestid;
/**
* Setup function- we will create a course and add an assign instance to it.
*/
protected function setUp() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Login as user.
$this->setUser($requester->id);
// Test data request creation.
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$this->requestid = $datarequest->get('id');
$this->requester = $requester;
// Log out the user and set force login to true.
$this->setUser();
}
/**
* Test for external::approve_data_request() with the user not logged in.
*/
public function test_approve_data_request_not_logged_in() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Log out the user and set force login to true.
$this->setUser();
$this->expectException(require_login_exception::class);
external::approve_data_request($this->requestid);
external::approve_data_request($datarequest->get('id'));
}
/**
* Test for external::approve_data_request() with the user not having a DPO role.
*/
public function test_approve_data_request_not_dpo() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as the requester.
$this->setUser($this->requester->id);
$this->setUser($requester);
$this->expectException(required_capability_exception::class);
external::approve_data_request($this->requestid);
external::approve_data_request($datarequest->get('id'));
}
/**
* Test for external::approve_data_request() for request that's not ready for approval
*/
public function test_approve_data_request_not_waiting_for_approval() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$this->expectException(moodle_exception::class);
external::approve_data_request($this->requestid);
external::approve_data_request($datarequest->get('id'));
}
/**
* Test for external::approve_data_request()
*/
public function test_approve_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($this->requestid);
api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -114,10 +128,13 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::approve_data_request() for a non-existent request ID.
*/
public function test_approve_data_request_non_existent() {
$this->resetAfterTest();
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($this->requestid + 1);
$result = external::approve_data_request(1);
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
@@ -129,13 +146,21 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::cancel_data_request() of another user.
*/
public function test_cancel_data_request_other_user() {
$generator = $this->getDataGenerator();
$otheruser = $generator->create_user();
$this->resetAfterTest();
// Login as another user.
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as other user.
$this->setUser($otheruser);
$result = external::cancel_data_request($this->requestid);
$result = external::cancel_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
@@ -143,14 +168,107 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
}
/**
* Test cancellation of a request where you are the requester of another user's data.
*/
public function test_cancel_data_request_other_user_as_requester() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Assign requester as otheruser'sparent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$result = external::cancel_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
}
/**
* Test cancellation of a request where you are the requester of another user's data.
*/
public function test_cancel_data_request_requester_lost_permissions() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Assign requester as otheruser'sparent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
$this->setUser($requester);
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Unassign the role.
role_unassign($parentrole, $requester->id, \context_user::instance($otheruser->id)->id);
// This user can no longer make the request.
$this->expectException(required_capability_exception::class);
$result = external::cancel_data_request($datarequest->get('id'));
}
/**
* Test cancellation of a request where you are the requester of another user's data.
*/
public function test_cancel_data_request_other_user_as_child() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
$comment = 'sample comment';
// Assign requester as otheruser'sparent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id));
// Test data request creation.
$this->setUser($otheruser);
$datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$result = external::cancel_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
}
/**
* Test for external::cancel_data_request()
*/
public function test_cancel_data_request() {
// Login as the requester.
$this->setUser($this->requester);
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Test cancellation.
$this->setUser($requester);
$result = external::cancel_data_request($datarequest->get('id'));
$result = external::cancel_data_request($this->requestid);
$return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -160,10 +278,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test contact DPO.
*/
public function test_contact_dpo() {
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$this->setUser($user);
$message = 'Hello world!';
$result = external::contact_dpo($message);
$return = (object) external_api::clean_returnvalue(external::contact_dpo_returns(), $result);
@@ -175,10 +295,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test contact DPO with message containing invalid input.
*/
public function test_contact_dpo_with_nasty_input() {
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
$this->resetAfterTest();
$generator = new testing_data_generator();
$user = $generator->create_user();
$this->setUser($user);
$this->expectException('invalid_parameter_exception');
external::contact_dpo('de<>\\..scription');
}
@@ -187,38 +309,77 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::deny_data_request() with the user not logged in.
*/
public function test_deny_data_request_not_logged_in() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
// Test data request creation.
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Log out.
$this->setUser();
$this->expectException(require_login_exception::class);
external::deny_data_request($this->requestid);
external::deny_data_request($datarequest->get('id'));
}
/**
* Test for external::deny_data_request() with the user not having a DPO role.
*/
public function test_deny_data_request_not_dpo() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as the requester.
$this->setUser($this->requester->id);
$this->setUser($requester);
$this->expectException(required_capability_exception::class);
external::deny_data_request($this->requestid);
external::deny_data_request($datarequest->get('id'));
}
/**
* Test for external::deny_data_request() for request that's not ready for approval
*/
public function test_deny_data_request_not_waiting_for_approval() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$this->expectException(moodle_exception::class);
external::deny_data_request($this->requestid);
external::deny_data_request($datarequest->get('id'));
}
/**
* Test for external::deny_data_request()
*/
public function test_deny_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($this->requestid);
api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::approve_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -228,10 +389,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::deny_data_request() for a non-existent request ID.
*/
public function test_deny_data_request_non_existent() {
$this->resetAfterTest();
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::deny_data_request($this->requestid + 1);
$result = external::deny_data_request(1);
$return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
@@ -243,34 +406,62 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::get_data_request() with the user not logged in.
*/
public function test_get_data_request_not_logged_in() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
$this->setUser();
$this->expectException(require_login_exception::class);
external::get_data_request($this->requestid);
external::get_data_request($datarequest->get('id'));
}
/**
* Test for external::get_data_request() with the user not having a DPO role.
*/
public function test_get_data_request_not_dpo() {
$generator = $this->getDataGenerator();
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$otheruser = $generator->create_user();
// Login as the requester.
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Login as the otheruser.
$this->setUser($otheruser);
$this->expectException(required_capability_exception::class);
external::get_data_request($this->requestid);
external::get_data_request($datarequest->get('id'));
}
/**
* Test for external::get_data_request()
*/
public function test_get_data_request() {
$this->resetAfterTest();
$generator = new testing_data_generator();
$requester = $generator->create_user();
$comment = 'sample comment';
$this->setUser($requester);
$datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$result = external::get_data_request($this->requestid);
$result = external::get_data_request($datarequest->get('id'));
$return = (object) external_api::clean_returnvalue(external::get_data_request_returns(), $result);
$this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $return->result['type']);
$this->assertEquals('sample comment', $return->result['comments']);
$this->assertEquals($this->requester->id, $return->result['userid']);
$this->assertEquals($this->requester->id, $return->result['requestedby']);
$this->assertEquals($requester->id, $return->result['userid']);
$this->assertEquals($requester->id, $return->result['requestedby']);
$this->assertEmpty($return->warnings);
}
@@ -278,10 +469,12 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::get_data_request() for a non-existent request ID.
*/
public function test_get_data_request_non_existent() {
$this->resetAfterTest();
// Admin as DPO. (The default when no one's assigned as a DPO in the site).
$this->setAdminUser();
$this->expectException(dml_missing_record_exception::class);
external::get_data_request($this->requestid + 1);
external::get_data_request(1);
}
/**
@@ -289,6 +482,8 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* when called by a user that doesn't have the manage registry capability.
*/
public function test_set_context_defaults_no_capability() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
@@ -307,6 +502,8 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* @param bool $override Whether to override instances.
*/
public function test_set_context_defaults($modulelevel, $override) {
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
@@ -364,9 +561,11 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* when called by a user that doesn't have the manage registry capability.
*/
public function test_get_category_options_no_capability() {
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(required_capability_exception::class);
external::get_category_options(true, true);
}
@@ -391,6 +590,7 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* @param bool $includenotset Whether "Not set" would be included to the options.
*/
public function test_get_category_options($includeinherit, $includenotset) {
$this->resetAfterTest();
$this->setAdminUser();
// Prepare our expected options.
@@ -436,6 +636,7 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* when called by a user that doesn't have the manage registry capability.
*/
public function test_get_purpose_options_no_capability() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$this->setUser($user);
@@ -451,6 +652,7 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* @param bool $includenotset Whether "Not set" would be included to the options.
*/
public function test_get_purpose_options($includeinherit, $includenotset) {
$this->resetAfterTest();
$this->setAdminUser();
// Prepare our expected options.
@@ -518,6 +720,7 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* @param bool $nodefaults Whether to fetch only activities that don't have defaults.
*/
public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
$this->resetAfterTest();
$this->setAdminUser();
$category = api::create_category((object)['name' => 'Test category']);
@@ -565,21 +768,25 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::bulk_approve_data_requests().
*/
public function test_bulk_approve_data_requests() {
$generator = new testing_data_generator();
$requester1 = $generator->create_user();
$comment1 = 'sample comment';
// Login as requester2.
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
// Create delete data request.
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requestid2 = $this->requestid;
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
// Approve the requests.
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -589,48 +796,100 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::bulk_approve_data_requests() for a non-existent request ID.
*/
public function test_bulk_approve_data_requests_non_existent() {
$generator = new testing_data_generator();
$requester1 = $generator->create_user();
$comment1 = 'sample comment';
// Login as requester2.
$this->setUser($requester1->id);
// Create delete data request.
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
$requestid1 = $datarequest1->get('id');
$requestid2 = $this->requestid;
$this->resetAfterTest();
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::bulk_approve_data_requests([$requestid1 + 1, $requestid2]);
$result = external::bulk_approve_data_requests([42]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
$warning = reset($return->warnings);
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
$this->assertEquals($requestid1 + 1, $warning['item']);
$this->assertEquals(42, $warning['item']);
}
/**
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
*/
public function test_bulk_approve_data_requests_no_permission() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Approve the requests.
$uut = $this->getDataGenerator()->create_user();
$this->setUser($uut);
$this->expectException(required_capability_exception::class);
$result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
}
/**
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
*/
public function test_bulk_approve_data_requests_own_request() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Deny the requests.
$this->setUser($requester1);
$this->expectException(required_capability_exception::class);
$result = external::bulk_approve_data_requests([$requestid1]);
}
/**
* Test for external::bulk_deny_data_requests().
*/
public function test_bulk_deny_data_requests() {
$generator = new testing_data_generator();
$requester1 = $generator->create_user();
$comment1 = 'sample comment';
// Login as requester2.
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
// Create delete data request.
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requestid2 = $this->requestid;
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
// Deny the requests.
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertTrue($return->result);
$this->assertEmpty($return->warnings);
@@ -640,26 +899,73 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
* Test for external::bulk_deny_data_requests() for a non-existent request ID.
*/
public function test_bulk_deny_data_requests_non_existent() {
$generator = new testing_data_generator();
$requester1 = $generator->create_user();
$comment1 = 'sample comment';
// Login as requester2.
$this->setUser($requester1->id);
// Create delete data request.
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
$requestid1 = $datarequest1->get('id');
$requestid2 = $this->requestid;
$this->resetAfterTest();
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
$result = external::bulk_deny_data_requests([$requestid1 + 1, $requestid2]);
$result = external::bulk_deny_data_requests([42]);
$return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
$this->assertFalse($return->result);
$this->assertCount(1, $return->warnings);
$warning = reset($return->warnings);
$this->assertEquals('errorrequestnotfound', $warning['warningcode']);
$this->assertEquals($requestid1 + 1, $warning['item']);
$this->assertEquals(42, $warning['item']);
}
/**
* Test for external::bulk_deny_data_requests() for a user without permission to deny requests.
*/
public function test_bulk_deny_data_requests_no_permission() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Deny the requests.
$uut = $this->getDataGenerator()->create_user();
$this->setUser($uut);
$this->expectException(required_capability_exception::class);
$result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
}
/**
* Test for external::bulk_deny_data_requests() for a user cannot approve their own request.
*/
public function test_bulk_deny_data_requests_own_request() {
$this->resetAfterTest();
// Create delete data requests.
$requester1 = $this->getDataGenerator()->create_user();
$this->setUser($requester1->id);
$datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid1 = $datarequest1->get('id');
$requester2 = $this->getDataGenerator()->create_user();
$this->setUser($requester2->id);
$datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment');
$requestid2 = $datarequest2->get('id');
$this->setAdminUser();
api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
// Deny the requests.
$this->setUser($requester1);
$this->expectException(required_capability_exception::class);
$result = external::bulk_deny_data_requests([$requestid1]);
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2017051552;
$plugin->version = 2017051553;
$plugin->requires = 2017051507.00; // Moodle 3.3.7 (Build: 20180517) and upwards.
$plugin->component = 'tool_dataprivacy';
+2 -2
View File
@@ -4146,8 +4146,8 @@ function delete_user(stdClass $user) {
// Don't trigger update event, as user is being deleted.
user_update_user($updateuser, false, false);
// Now do a final accesslib cleanup - removes all role assignments in user context and context itself.
context_helper::delete_instance(CONTEXT_USER, $user->id);
// Delete all content associated with the user context, but not the context itself.
$usercontext->delete_content();
// Any plugin that needs to cleanup should register this event.
// Trigger event.