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