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.
/**
* 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.
*
* @return \tool_dataprivacy\purpose.
*/
public static function create_purpose(stdClass $record) {
- self::check_can_manage_data_registry();
-
$purpose = new purpose(0, $record);
$purpose->create();
* @return \tool_dataprivacy\purpose.
*/
public static function update_purpose(stdClass $record) {
- self::check_can_manage_data_registry();
-
if (!isset($record->sensitivedatareasons)) {
$record->sensitivedatareasons = '';
}
* @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.');
* @return \tool_dataprivacy\purpose[]
*/
public static function get_purposes() {
- self::check_can_manage_data_registry();
-
return purpose::get_records([], 'name', 'ASC');
}
* @return \tool_dataprivacy\category.
*/
public static function create_category(stdClass $record) {
- self::check_can_manage_data_registry();
-
$category = new category(0, $record);
$category->create();
* @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);
* @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.');
* @return \tool_dataprivacy\category[]
*/
public static function get_categories() {
- self::check_can_manage_data_registry();
-
return category::get_records([], 'name', 'ASC');
}
* @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);
* @return null
*/
public static function unset_context_instance(context_instance $instance) {
- self::check_can_manage_data_registry($instance->get('contextid'));
$instance->delete();
}
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');
* @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;
}
* @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;
}
* @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;
}
* @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;
}
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.
*
* @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();
}
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);
* @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) {
// 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;
+ 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);
}
- // 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);
}
+
+ // 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.
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) : expired_context {
+ $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) : expired_context {
+ 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() : bool {
+ return ($this->get('status') == self::STATUS_APPROVED);
+ }
+
+ /**
+ * Check whether this expired_context record has already been cleaned.
+ *
+ * @return bool
+ */
+ public function is_complete() : bool {
+ return ($this->get('status') == self::STATUS_CLEANED);
+ }
}
* @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;
+ /** @var progress_trace The log progress tracer */
+ protected $progresstracer = null;
+
+ /** @var manager The privacy manager */
+ protected $manager = null;
+
/**
- * Returns the list of expired context instances.
+ * Flag expired contexts as expired.
*
- * @return \stdClass[]
+ * @return int[] The number of contexts flagged as expired for courses, and users.
+ */
+ public function flag_expired_contexts() : array {
+ if (!$this->check_requirements()) {
+ return [0, 0];
+ }
+
+ // 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++;
+ }
+ }
+
+ $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.
*/
- abstract protected function get_expired_contexts();
+ 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();
+ }
+ }
/**
- * Specify with context levels this expired contexts manager is deleting.
+ * Get the full nested set of expiry data relating to all contexts.
*
- * @return int[]
+ * @param string $contextpath A contexpath to restrict results to
+ * @return \stdClass[]
*/
- abstract protected function get_context_levels();
+ protected static function get_nested_expiry_info($contextpath = '') : array {
+ $coursepaths = self::get_nested_expiry_info_for_courses($contextpath);
+ $userpaths = self::get_nested_expiry_info_for_user($contextpath);
+
+ return array_merge($coursepaths, $userpaths);
+ }
/**
- * Flag expired contexts as expired.
+ * Get the full nested set of expiry data relating to course-related contexts.
*
- * @return int The number of contexts flagged as expired.
+ * @param string $contextpath A contexpath to restrict results to
+ * @return \stdClass[]
*/
- public function flag_expired() {
+ protected static function get_nested_expiry_info_for_courses($contextpath = '') : array {
+ global $DB;
- if (!$this->check_requirements()) {
- return 0;
- }
+ $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', "'/%'");
- $contexts = $this->get_expired_contexts();
- foreach ($contexts as $context) {
- api::create_expired_context($context->id);
+ // 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}/%";
}
- return count($contexts);
+ $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);
}
/**
- * Deletes the expired contexts.
+ * Get the full nested set of expiry data.
*
- * @return int The number of deleted contexts.
+ * @param string $contextpath A contexpath to restrict results to
+ * @return \stdClass[]
*/
- public function delete() {
+ protected static function get_nested_expiry_info_for_user($contextpath = '') : array {
+ global $DB;
- $numprocessed = 0;
+ $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]);
- if (!$this->check_requirements()) {
- return $numprocessed;
+ // 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;
}
- $privacymanager = new manager();
- $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
+ $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";
- foreach ($this->get_context_levels() as $level) {
+ 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(string $sql, array $params) : array {
+ 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);
- $expiredcontexts = expired_context::get_records_by_contextlevel($level, expired_context::STATUS_APPROVED);
+ 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);
- foreach ($expiredcontexts as $expiredctx) {
+ 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 (!$this->delete_expired_context($privacymanager, $expiredctx)) {
+ 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;
}
- $numprocessed += 1;
- if ($numprocessed == self::DELETE_LIMIT) {
- // Close the recordset.
- $expiredcontexts->close();
- break 2;
+ $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) : bool {
+ $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.
+ */
+ public function process_approved_deletions() : array {
+ if (!$this->check_requirements()) {
+ return [0, 0];
+ }
+
+ $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;
+ }
+
+ if ($this->delete_expired_context($expiredctx)) {
+ if ($context instanceof \context_user) {
+ $usercount++;
+ } else {
+ $coursecount++;
+ }
+
+ $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'));
+
+ $this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true));
- $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
- if (!$context) {
- api::delete_expired_context($expiredctx->get('contextid'));
+ // 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() : bool {
+ $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(string $period, int $comparisondate) : bool {
+ $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, int $comparisondate = 0) : expiry_info {
+ 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) : bool {
+ $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() : 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() : int {
+ return self::DELETE_LIMIT;
+ }
+
+ /**
+ * Get the progress tracer.
+ *
+ * @return \progress_trace
+ */
+ protected function get_progress() : \progress_trace {
+ 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) : expired_contexts_manager {
+ $this->progresstracer = $trace;
+
+ return $this;
+ }
}
+++ /dev/null
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * 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;
- }
-}
+++ /dev/null
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * 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;
- }
-}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Expiry Data.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 <andrew@nicols.co.uk>
+ * @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(bool $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() : bool {
+ return $this->isexpired;
+ }
+
+ /**
+ * Whether any part of this context has expired.
+ *
+ * @return bool
+ */
+ public function is_any_expired() : bool {
+ 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) : expiry_info {
+ 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;
+ }
+}
]);
$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 {
]);
$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.
'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();
'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 [
'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();
'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 [
'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();
$contextlevel = api::set_contextlevel($validateddata);
} else if ($errors = $mform->is_validated()) {
$warnings[] = json_encode($errors);
- throw new moodle_exception('generalerror');
}
if ($contextlevel) {
'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();
$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);
]);
$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)) {
$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;
+ }
}
}
// 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);
$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);
/**
* 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");
}
}
* 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");
}
}
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);
// 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) {
$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.';
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));
}
}
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);
$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);
* 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();
*/
public function test_get_site_dpos() {
global $DB;
+
+ $this->resetAfterTest();
+
$generator = new testing_data_generator();
$u1 = $generator->create_user();
$u2 = $generator->create_user();
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');
public function test_approve_data_request() {
global $DB;
+ $this->resetAfterTest();
+
$generator = new testing_data_generator();
$s1 = $generator->create_user();
$u1 = $generator->create_user();
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();
* 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();
$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());
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));
$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.
* 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';
public function test_create_data_request_by_dpo() {
global $USER;
+ $this->resetAfterTest();
+
$generator = new testing_data_generator();
$user = $generator->create_user();
$comment = 'sample comment';
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();
* 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';
$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().
*
* @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();
* @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();
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));
* @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).
$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();
$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();
public function test_context_instances() {
global $DB;
+ $this->resetAfterTest();
+
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
public function test_contextlevel() {
global $DB;
+ $this->resetAfterTest();
+
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
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);
$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.
* @return null
*/
public function test_effective_context() {
+ $this->resetAfterTest();
+
$this->setAdminUser();
list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
$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']);
* 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();
* 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();
* @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();
* @return \stdClass
*/
protected function setup_test_add_request_contexts_with_status($type) {
+ $this->resetAfterTest();
+
$this->setAdminUser();
// User under test.
use tool_dataprivacy\api;
use tool_dataprivacy\data_registry;
use tool_dataprivacy\expired_context;
+use tool_dataprivacy\purpose;
+use tool_dataprivacy\category;
+use tool_dataprivacy\contextlevel;
defined('MOODLE_INTERNAL') || die();
global $CFG;
class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
/**
- * setUp.
+ * Setup the basics with the specified retention period.
+ *
+ * @param string $system Retention policy for the system.
+ * @param string $user Retention policy for users.
+ * @param string $course Retention policy for courses.
+ * @param string $activity Retention policy for activities.
*/
- public function setUp() {
+ protected function setup_basics(string $system, string $user, string $course, string $activity = null) : array {
$this->resetAfterTest();
- $this->setAdminUser();
+
+ $purposes = [];
+ $purposes[] = $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM);
+ $purposes[] = $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER);
+ $purposes[] = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
+ if (null !== $activity) {
+ $purposes[] = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
+ }
+
+ return $purposes;
}
/**
- * Test expired users flagging and deletion.
+ * Create a retention period and set it for the specified context level.
*
- * @return null
+ * @param string $retention
+ * @param int $contextlevel
+ * @return purpose
*/
- public function test_expired_users() {
- global $DB;
+ protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
+ $purpose = new purpose(0, (object) [
+ 'name' => 'Test purpose ' . rand(1, 1000),
+ 'retentionperiod' => $retention,
+ 'lawfulbases' => 'gdpr_art_6_1_a',
+ ]);
+ $purpose->create();
- $purpose = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
- $cat = api::create_category((object)['name' => 'a']);
+ $cat = new category(0, (object) ['name' => 'Test category']);
+ $cat->create();
- $record = (object)[
- 'purposeid' => $purpose->get('id'),
- 'categoryid' => $cat->get('id'),
- 'contextlevel' => CONTEXT_SYSTEM,
- ];
- api::set_contextlevel($record);
- $record->contextlevel = CONTEXT_USER;
- api::set_contextlevel($record);
-
- $userdata = ['lastaccess' => '123'];
- $user1 = $this->getDataGenerator()->create_user($userdata);
- $user2 = $this->getDataGenerator()->create_user($userdata);
- $user3 = $this->getDataGenerator()->create_user($userdata);
- $user4 = $this->getDataGenerator()->create_user($userdata);
- $user5 = $this->getDataGenerator()->create_user();
-
- $course1 = $this->getDataGenerator()->create_course();
- // Old course.
- $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']);
- // Ongoing course.
- $course3 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => time() + YEARSECS]);
-
- $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
- $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
- $this->getDataGenerator()->enrol_user($user3->id, $course2->id, 'student');
- $this->getDataGenerator()->enrol_user($user4->id, $course3->id, 'student');
-
- // Add an activity and some data for user 2.
- $assignmod = $this->getDataGenerator()->create_module('assign', ['course' => $course2->id]);
- $data = (object) [
- 'assignment' => $assignmod->id,
- 'userid' => $user2->id,
- 'timecreated' => time(),
- 'timemodified' => time(),
- 'status' => 'new',
- 'groupid' => 0,
- 'attemptnumber' => 0,
- 'latest' => 1,
- ];
- $DB->insert_record('assign_submission', $data);
- // We should have one record in the assign submission table.
- $this->assertEquals(1, $DB->count_records('assign_submission'));
+ if ($contextlevel <= CONTEXT_USER) {
+ $record = (object) [
+ 'purposeid' => $purpose->get('id'),
+ 'categoryid' => $cat->get('id'),
+ 'contextlevel' => $contextlevel,
+ ];
+ api::set_contextlevel($record);
+ } else {
+ list($purposevar, ) = data_registry::var_names_from_context(
+ \context_helper::get_class_for_level(CONTEXT_COURSE)
+ );
+ set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
+ }
+
+ return $purpose;
+ }
+
+ /**
+ * Ensure that a user with no lastaccess is not flagged for deletion.
+ */
+ public function test_flag_not_setup() {
+ $this->resetAfterTest();
+
+ $user = $this->getDataGenerator()->create_user();
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with no lastaccess is not flagged for deletion.
+ */
+ public function test_flag_user_no_lastaccess() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user();
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a recent lastaccess is not flagged for deletion.
+ */
+ public function test_flag_user_recent_lastaccess() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a lastaccess in the past is flagged for deletion.
+ */
+ public function test_flag_user_past_lastaccess() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ // Although there is a block in the user context, everything in the user context is regarded as one.
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a lastaccess in the past but active enrolments is not flagged for deletion.
+ */
+ public function test_flag_user_past_lastaccess_still_enrolled() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enddate' => time() + YEARSECS]);
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $otheruser = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a lastaccess in the past and expired enrolments.
+ */
+ public function test_flag_user_past_lastaccess_unexpired_past_enrolment() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'P1Y');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $otheruser = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a lastaccess in the past and expired enrolments.
+ */
+ public function test_flag_user_past_lastaccess_expired_enrolled() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $otheruser = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(1, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
+ * correctly.
+ */
+ public function test_flag_user_past_lastaccess_missing_enddate_required() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course();
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $otheruser = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Ensure that course end dates are not required.
+ set_config('requireallenddatesforuserdeletion', 1, 'tool_dataprivacy');
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected
+ * correctly when the end date is not required.
+ */
+ public function test_flag_user_past_lastaccess_missing_enddate_not_required() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course();
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $otheruser = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student');
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Ensure that course end dates are required.
+ set_config('requireallenddatesforuserdeletion', 0, 'tool_dataprivacy');
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a user with a recent lastaccess is not flagged for deletion.
+ */
+ public function test_flag_user_recent_lastaccess_existing_record() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]);
+ $usercontext = \context_user::instance($user->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredcontext->save();
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ $this->expectException('dml_missing_record_exception');
+ new expired_context($expiredcontext->get('id'));
+ }
+
+ /**
+ * Ensure that a user with a recent lastaccess is not flagged for deletion.
+ */
+ public function test_flag_user_retention_changed() {
+ $this->resetAfterTest();
+
+ list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+
+ $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
+ $this->assertNotFalse($expiredcontext);
+
+ // Increase the retention period to 5 years.
+ $userpurpose->set('retentionperiod', 'P5Y');
+ $userpurpose->save();
+
+ // Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased.
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ // The expiry record will now have been removed.
+ $this->expectException('dml_missing_record_exception');
+ new expired_context($expiredcontext->get('id'));
+ }
+
+ /**
+ * Ensure that a user with a historically expired expired block record child is cleaned up.
+ */
+ public function test_flag_user_historic_block_unapproved() {
+ $this->resetAfterTest();
+
+ list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $blockcontext = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Create an existing expired_context which has not been approved for the block.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $blockcontext->id,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredcontext->save();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+
+ $expiredblockcontext = expired_context::get_record(['contextid' => $blockcontext->id]);
+ $this->assertFalse($expiredblockcontext);
+
+ $expiredusercontext = expired_context::get_record(['contextid' => $usercontext->id]);
+ $this->assertNotFalse($expiredusercontext);
+ }
+
+ /**
+ * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
+ */
+ public function test_flag_user_historic_unexpired_child() {
+ $this->resetAfterTest();
+
+ list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $blockcontext = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+
+ $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]);
+ $this->assertNotFalse($expiredcontext);
+ }
+
+ /**
+ * Ensure that a course with no end date is not flagged.
+ */
+ public function test_flag_course_no_enddate() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course();
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a course with an end date in the distant past, but a child which is unexpired is not flagged.
+ */
+ public function test_flag_course_past_enddate_future_child() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'P5Y');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a course with an end date in the distant past is flagged.
+ */
+ public function test_flag_course_past_enddate() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(2, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a course with an end date in the distant past is flagged.
+ */
+ public function test_flag_course_past_enddate_multiple() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course1 = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]);
+
+ $course2 = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(4, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a course with an end date in the future is not flagged.
+ */
+ public function test_flag_course_future_enddate() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a course with an end date in the future is not flagged.
+ */
+ public function test_flag_course_recent_unexpired_enddate() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a site not setup will not process anything.
+ */
+ public function test_process_not_setup() {
+ $this->resetAfterTest();
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+ }
+
+ /**
+ * Ensure that a user with no lastaccess is not flagged for deletion.
+ */
+ public function test_process_none_approved() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
- // Users without lastaccess are skipped as well as users enroled in courses with no end date.
- $expired = new \tool_dataprivacy\expired_user_contexts();
- $numexpired = $expired->flag_expired();
- $this->assertEquals(2, $numexpired);
- $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED]));
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
- // Approve user2 to be deleted.
- $user2ctx = \context_user::instance($user2->id);
- $expiredctx = expired_context::get_record(['contextid' => $user2ctx->id]);
- api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED);
- $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_APPROVED]));
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+ }
+
+ /**
+ * Ensure that a user with no lastaccess is not flagged for deletion.
+ */
+ public function test_process_no_context() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => -1,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $this->expectException('dml_missing_record_exception');
+ new expired_context($expiredcontext->get('id'));
+ }
+
+ /**
+ * Ensure that a user context previously flagged as approved is removed.
+ */
+ public function test_process_user_context() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $blockcontext = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->exactly(2))
+ ->method('delete_data_for_all_users_in_context')
+ ->withConsecutive(
+ [$blockcontext],
+ [$usercontext]
+ );
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(1, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+
+ // Flag all expired contexts again.
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ // Ensure that the deleted context record is still present.
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a course context previously flagged as approved is removed.
+ */
+ public function test_process_course_context() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->once())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a user context previously flagged as approved is not removed if the user then logs in.
+ */
+ public function test_process_user_context_logged_in_after_approval() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ // Now bump the user's last login time.
+ $this->setUser($user);
+ user_accesstime_log();
+ $this->setUser();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $this->expectException('dml_missing_record_exception');
+ new expired_context($expiredcontext->get('id'));
+ }
+
+ /**
+ * Ensure that a user context previously flagged as approved is not removed if the purpose has changed.
+ */
+ public function test_process_user_context_changed_after_approved() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $context = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ // Now make the user a site admin.
+ $admins = explode(',', get_config('moodle', 'siteadmins'));
+ $admins[] = $user->id;
+ set_config('siteadmins', implode(',', $admins));
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $this->expectException('dml_missing_record_exception');
+ new expired_context($expiredcontext->get('id'));
+ }
+
+ /**
+ * Ensure that a user with a historically expired expired block record child is cleaned up.
+ */
+ public function test_process_user_historic_block_unapproved() {
+ $this->resetAfterTest();
+
+ list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $blockcontext = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Create an expired_context for the user.
+ $expiredusercontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredusercontext->save();
+
+ // Create an existing expired_context which has not been approved for the block.
+ $expiredblockcontext = new expired_context(0, (object) [
+ 'contextid' => $blockcontext->id,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredblockcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->exactly(2))
+ ->method('delete_data_for_all_users_in_context')
+ ->withConsecutive(
+ [$blockcontext],
+ [$usercontext]
+ );
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(1, $processedusers);
+
+ $updatedcontext = new expired_context($expiredusercontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a user with a block which has a default retention period which has not expired, is still expired.
+ */
+ public function test_process_user_historic_unexpired_child() {
+ $this->resetAfterTest();
+
+ list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ $this->setUser($user);
+ $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+ $blockcontext = \context_block::instance($block->instance->id);
+ $this->setUser();
+
+ // Create an expired_context for the user.
+ $expiredusercontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredusercontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->exactly(2))
+ ->method('delete_data_for_all_users_in_context')
+ ->withConsecutive(
+ [$blockcontext],
+ [$usercontext]
+ );
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(1, $processedusers);
+
+ $updatedcontext = new expired_context($expiredusercontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
+ * updated.
+ */
+ public function test_process_course_context_updated() {
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+
+ $coursepurpose = $purposes[2];
+ $coursepurpose->set('retentionperiod', 'P5Y');
+ $coursepurpose->save();
+
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
- // Delete expired contexts.
- $deleted = $expired->delete();
- $this->assertEquals(1, $deleted);
- $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED]));
- $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_CLEANED]));
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
- // No new records are generated.
- $numexpired = $expired->flag_expired();
- $this->assertEquals(0, $numexpired);
- $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired'));
- $deleted = $expired->delete();
- $this->assertEquals(0, $deleted);
+ // No change - we just can't process it until the children have finished.
+ $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
+ * updated.
+ */
+ public function test_process_course_context_outstanding_children() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+
+ // No change - we just can't process it until the children have finished.
+ $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
+ }
- // No user data left in mod_assign.
- $this->assertEquals(0, $DB->count_records('assign_submission'));
+ /**
+ * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
+ * updated.
+ */
+ public function test_process_course_context_pending_children() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+
+ // Create an existing expired_context for the course.
+ $expiredcoursecontext = new expired_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcoursecontext->save();
+
+ // And for the forum.
+ $expiredforumcontext = new expired_context(0, (object) [
+ 'contextid' => $forumcontext->id,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredforumcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
+
+ // No change - we just can't process it until the children have finished.
+ $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
+ }
- // The user is deleted.
- $deleteduser = \core_user::get_user($user2->id, 'id, deleted', IGNORE_MISSING);
- $this->assertEquals(1, $deleteduser->deleted);
+ /**
+ * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is
+ * updated.
+ */
+ public function test_process_course_context_approved_children() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+
+ // Create an existing expired_context for the course.
+ $expiredcoursecontext = new expired_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcoursecontext->save();
+
+ // And for the forum.
+ $expiredforumcontext = new expired_context(0, (object) [
+ 'contextid' => $forumcontext->id,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredforumcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->exactly(2))
+ ->method('delete_data_for_all_users_in_context')
+ ->withConsecutive(
+ [$forumcontext],
+ [$coursecontext]
+ );
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+ $manager->set_progress(new \null_progress_trace());
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+
+ // Initially only the forum will be processed.
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredforumcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+
+ // The course won't have been processed yet.
+ $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
+
+ // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts.
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+ $updatedcontext = new expired_context($expiredcoursecontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Test that the can_process_deletion function returns expected results.
+ *
+ * @dataProvider can_process_deletion_provider
+ * @param int $status
+ * @param bool $expected
+ */
+ public function test_can_process_deletion($status, $expected) {
+ $purpose = new expired_context(0, (object) [
+ 'status' => $status,
+
+ 'contextid' => \context_system::instance()->id,
+ ]);
+
+ $this->assertEquals($expected, $purpose->can_process_deletion());
+ }
+
+ /**
+ * Data provider for the can_process_deletion tests.
+ *
+ * @return array
+ */
+ public function can_process_deletion_provider() : array {
+ return [
+ 'Pending' => [
+ expired_context::STATUS_EXPIRED,
+ false,
+ ],
+ 'Approved' => [
+ expired_context::STATUS_APPROVED,
+ true,
+ ],
+ 'Complete' => [
+ expired_context::STATUS_CLEANED,
+ false,
+ ],
+ ];
}
/**
- * Test expired course and course stuff flagging and deletion.
+ * Test that the is_complete function returns expected results.
*
- * @return null
+ * @dataProvider is_complete_provider
+ * @param int $status
+ * @param bool $expected
*/
- public function test_expired_course_related_contexts() {
- global $DB;
+ public function test_is_complete($status, $expected) {
+ $purpose = new expired_context(0, (object) [
+ 'status' => $status,
+ 'contextid' => \context_system::instance()->id,
+ ]);
- $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
- $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
- $cat = api::create_category((object)['name' => 'a']);
+ $this->assertEquals($expected, $purpose->is_complete());
+ }
- $record = (object)[
- 'purposeid' => $purpose1->get('id'),
- 'categoryid' => $cat->get('id'),
- 'contextlevel' => CONTEXT_SYSTEM,
+ /**
+ * Data provider for the is_complete tests.
+ *
+ * @return array
+ */
+ public function is_complete_provider() : array {
+ return [
+ 'Pending' => [
+ expired_context::STATUS_EXPIRED,
+ false,
+ ],
+ 'Approved' => [
+ expired_context::STATUS_APPROVED,
+ false,
+ ],
+ 'Complete' => [
+ expired_context::STATUS_CLEANED,
+ true,
+ ],
];
- api::set_contextlevel($record);
-
- list($purposevar, $categoryvar) = data_registry::var_names_from_context(
- \context_helper::get_class_for_level(CONTEXT_COURSE)
- );
- set_config($purposevar, $purpose1->get('id'), 'tool_dataprivacy');
-
- // A lot more time for modules.
- list($purposevar, $categoryvar) = data_registry::var_names_from_context(
- \context_helper::get_class_for_level(CONTEXT_MODULE)
- );
- set_config($purposevar, $purpose2->get('id'), 'tool_dataprivacy');
-
- $course1 = $this->getDataGenerator()->create_course();
-
- // Course finished last week (so purpose1 retention period does delete stuff but purpose2 retention period does not).
- $dt = new \DateTime();
- $di = new \DateInterval('P7D');
- $dt->sub($di);
-
- $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => $dt->getTimestamp()]);
- $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
- $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
-
- // We want to override this last module instance purpose so we can test that modules are also
- // returned as expired.
- $forum2ctx = \context_module::instance($forum2->cmid);
- $record = (object)[
- 'purposeid' => $purpose1->get('id'),
- 'categoryid' => $cat->get('id'),
- 'contextid' => $forum2ctx->id,
+ }
+
+ /**
+ * Ensure that any orphaned records are removed once the context has been removed.
+ */
+ public function test_orphaned_records_are_cleared() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $context = \context_course::instance($course->id);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ $manager->set_progress(new \null_progress_trace());
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(1, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ // Ensure that the record currently exists.
+ $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
+ $this->assertNotFalse($expiredcontext);
+
+ // Approve it.
+ $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save();
+
+ // Process deletions.
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ // Ensure that the record still exists.
+ $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
+ $this->assertNotFalse($expiredcontext);
+
+ // Remove the actual course.
+ delete_course($course->id, false);
+
+ // The record will still exist until we flag it again.
+ $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
+ $this->assertNotFalse($expiredcontext);
+
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+ $expiredcontext = expired_context::get_record(['contextid' => $context->id]);
+ $this->assertFalse($expiredcontext);
+ }
+
+ /**
+ * Ensure that the progres tracer works as expected out of the box.
+ */
+ public function test_progress_tracer_default() {
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+
+ $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
+ $rcm = $rc->getMethod('get_progress');
+
+ $rcm->setAccessible(true);
+ $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager));
+ }
+
+ /**
+ * Ensure that the progres tracer works as expected when given a specific traer.
+ */
+ public function test_progress_tracer_set() {
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ $mytrace = new \null_progress_trace();
+ $manager->set_progress($mytrace);
+
+ $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class);
+ $rcm = $rc->getMethod('get_progress');
+
+ $rcm->setAccessible(true);
+ $this->assertSame($mytrace, $rcm->invoke($manager));
+ }
+
+ /**
+ * Creates an HTML block on a user.
+ *
+ * @param string $title
+ * @param string $body
+ * @param string $format
+ * @return \block_instance
+ */
+ protected function create_user_block($title, $body, $format) {
+ global $USER;
+
+ $configdata = (object) [
+ 'title' => $title,
+ 'text' => [
+ 'itemid' => 19,
+ 'text' => $body,
+ 'format' => $format,
+ ],
];
- api::set_context_instance($record);
-
- // Ongoing course.
- $course3 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => time()]);
- $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id));
-
- $expired = new \tool_dataprivacy\expired_course_related_contexts();
- $numexpired = $expired->flag_expired();
-
- // Only 1 module has expired.
- $this->assertEquals(1, $numexpired);
- $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED]));
-
- // Add a forum1 override to 1h retention period so both forum1 and course2 are also expired.
- $forum1ctx = \context_module::instance($forum1->cmid);
- $record->purposeid = $purpose1->get('id');
- $record->contextid = $forum1ctx->id;
- api::set_context_instance($record);
- $numexpired = $expired->flag_expired();
- $this->assertEquals(2, $numexpired);
- $this->assertEquals(3, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED]));
-
- // Approve forum1 to be deleted.
- $expiredctx = expired_context::get_record(['contextid' => $forum1ctx->id]);
- api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED);
-
- // Delete expired contexts.
- $deleted = $expired->delete();
- $this->assertEquals(1, $deleted);
- $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_CLEANED]));
-
- $expiredctx = expired_context::get_record(['contextid' => $forum2ctx->id]);
- api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED);
-
- $course2ctx = \context_course::instance($course2->id);
- $expiredctx = expired_context::get_record(['contextid' => $course2ctx->id]);
- api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED);
-
- // Delete expired contexts.
- $deleted = $expired->delete();
- $this->assertEquals(2, $deleted);
- $this->assertEquals(3, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_CLEANED]));
-
- // No new records are generated.
- $numexpired = $expired->flag_expired();
- $this->assertEquals(0, $numexpired);
- $this->assertEquals(3, $DB->count_records('tool_dataprivacy_ctxexpired'));
- $deleted = $expired->delete();
- $this->assertEquals(0, $deleted);
+ $this->create_block($this->construct_user_page($USER));
+ $block = $this->get_last_block_on_page($this->construct_user_page($USER));
+ $block = block_instance('html', $block->instance);
+ $block->instance_config_save((object) $configdata);
+
+ return $block;
+ }
+
+ /**
+ * Creates an HTML block on a page.
+ *
+ * @param \page $page Page
+ */
+ protected function create_block($page) {
+ $page->blocks->add_block_at_end_of_default_region('html');
+ }
+
+ /**
+ * Constructs a Page object for the User Dashboard.
+ *
+ * @param \stdClass $user User to create Dashboard for.
+ * @return \moodle_page
+ */
+ protected function construct_user_page(\stdClass $user) {
+ $page = new \moodle_page();
+ $page->set_context(\context_user::instance($user->id));
+ $page->set_pagelayout('mydashboard');
+ $page->set_pagetype('my-index');
+ $page->blocks->load_blocks();
+ return $page;
+ }
+
+ /**
+ * Get the last block on the page.
+ *
+ * @param \page $page Page
+ * @return \block_html Block instance object
+ */
+ protected function get_last_block_on_page($page) {
+ $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
+ $block = end($blocks);
+
+ return $block;
}
}
*/
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.
+ * Test for external::approve_data_request() with the user not logged in.
*/
- protected function setUp() {
+ public function test_approve_data_request_not_logged_in() {
$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.
+ $this->setUser($requester);
$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->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);
* 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);
* Test for external::cancel_data_request() of another user.
*/
public function test_cancel_data_request_other_user() {
- $generator = $this->getDataGenerator();
+ $this->resetAfterTest();
+
+ $generator = new testing_data_generator();
+ $requester = $generator->create_user();
$otheruser = $generator->create_user();
+ $comment = 'sample comment';
- // Login as another user.
+ // 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);
$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);
* Test contact DPO.
*/
public function test_contact_dpo() {
- $generator = $this->getDataGenerator();
+ $this->resetAfterTest();
+
+ $generator = new testing_data_generator();
$user = $generator->create_user();
- $this->setUser($user);
+ $this->setUser($user);
$message = 'Hello world!';
$result = external::contact_dpo($message);
$return = (object) external_api::clean_returnvalue(external::contact_dpo_returns(), $result);
* Test contact DPO with message containing invalid input.
*/
public function test_contact_dpo_with_nasty_input() {
- $generator = $this->getDataGenerator();
+ $this->resetAfterTest();
+
+ $generator = new testing_data_generator();
$user = $generator->create_user();
- $this->setUser($user);
+ $this->setUser($user);
$this->expectException('invalid_parameter_exception');
external::contact_dpo('de<>\\..scription');
}
* 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);
* 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);
* 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);
}
* 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);
}
/**
* 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);
* @param bool $override Whether to override instances.
*/
public function test_set_context_defaults($modulelevel, $override) {
+ $this->resetAfterTest();
+
$this->setAdminUser();
$generator = $this->getDataGenerator();
* 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);
}
* @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.
* 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);
* @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.
* @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']);
* 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->setUser($requester1->id);
- // Create delete data request.
- $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+ $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');
- $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);
* 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->setUser($requester1->id);
- // Create delete data request.
- $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
+ $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');
- $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);
* 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]);
}
}
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2018091100;
+$plugin->version = 2018092500;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';
// 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.