Merge branch 'MDL-62560-master'
This commit is contained in:
@@ -988,6 +988,7 @@ class api {
|
||||
*/
|
||||
public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
|
||||
$request = new data_request($requestid);
|
||||
$user = \core_user::get_user($request->get('userid'));
|
||||
foreach ($clcollection as $contextlist) {
|
||||
// Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
|
||||
$clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
|
||||
@@ -998,10 +999,14 @@ class api {
|
||||
foreach ($contextlist->get_contextids() as $contextid) {
|
||||
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
$context = \context::instance_by_id($contextid);
|
||||
if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
|
||||
$purpose = static::get_effective_context_purpose($context);
|
||||
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $user)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$context = new contextlist_context();
|
||||
$context->set('contextid', $contextid)
|
||||
->set('contextlistid', $contextlistid)
|
||||
@@ -1099,6 +1104,15 @@ class api {
|
||||
$contexts = [];
|
||||
}
|
||||
|
||||
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
|
||||
$context = \context::instance_by_id($record->contextid);
|
||||
$purpose = static::get_effective_context_purpose($context);
|
||||
// Data can only be deleted from it if the context is either expired, or unprotected.
|
||||
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$contexts[] = $record->contextid;
|
||||
$lastcomponent = $record->component;
|
||||
}
|
||||
@@ -1196,4 +1210,25 @@ class api {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the supplied date interval as a retention period.
|
||||
*
|
||||
* @param \DateInterval $interval
|
||||
* @return string
|
||||
*/
|
||||
public static function format_retention_period(\DateInterval $interval) : string {
|
||||
// It is one or another.
|
||||
if ($interval->y) {
|
||||
$formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
|
||||
} else if ($interval->m) {
|
||||
$formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
|
||||
} else if ($interval->d) {
|
||||
$formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
|
||||
} else {
|
||||
$formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
return $formattedtime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,12 +60,27 @@ class expired_context extends \core\persistent {
|
||||
* @return array
|
||||
*/
|
||||
protected static function define_properties() {
|
||||
return array(
|
||||
'contextid' => array(
|
||||
return [
|
||||
'contextid' => [
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The context id.',
|
||||
),
|
||||
'status' => array(
|
||||
],
|
||||
'defaultexpired' => [
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'Whether to default retention period for the purpose has been reached',
|
||||
'default' => 1,
|
||||
],
|
||||
'expiredroles' => [
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'This list of roles to include during deletion',
|
||||
'default' => '',
|
||||
],
|
||||
'unexpiredroles' => [
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'This list of roles to exclude during deletion',
|
||||
'default' => '',
|
||||
],
|
||||
'status' => [
|
||||
'choices' => [
|
||||
self::STATUS_EXPIRED,
|
||||
self::STATUS_APPROVED,
|
||||
@@ -73,8 +88,8 @@ class expired_context extends \core\persistent {
|
||||
],
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The deletion status of the context.',
|
||||
),
|
||||
);
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,21 +175,130 @@ class expired_context extends \core\persistent {
|
||||
return $DB->count_records_sql($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of role IDs for either expiredroles, or unexpiredroles.
|
||||
*
|
||||
* @param string $field
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
protected function set_roleids_for(string $field, array $roleids) : expired_context {
|
||||
$roledata = json_encode($roleids);
|
||||
|
||||
$this->raw_set($field, $roledata);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of role IDs for either expiredroles, or unexpiredroles.
|
||||
*
|
||||
* @param string $field
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_roleids_for(string $field) {
|
||||
$value = $this->raw_get($field);
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return json_decode($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of unexpired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
protected function set_unexpiredroles(array $roleids) : expired_context {
|
||||
$this->set_roleids_for('unexpiredroles', $roleids);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of role IDs to the list of expired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
public function add_expiredroles(array $roleids) : expired_context {
|
||||
$existing = $this->get('expiredroles');
|
||||
$newvalue = array_merge($existing, $roleids);
|
||||
|
||||
$this->set('expiredroles', $newvalue);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of role IDs to the list of unexpired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return unexpired_context
|
||||
*/
|
||||
public function add_unexpiredroles(array $roleids) : expired_context {
|
||||
$existing = $this->get('unexpiredroles');
|
||||
$newvalue = array_merge($existing, $roleids);
|
||||
|
||||
$this->set('unexpiredroles', $newvalue);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of expired role IDs.
|
||||
*
|
||||
* @param int[] $roleids
|
||||
* @return expired_context
|
||||
*/
|
||||
protected function set_expiredroles(array $roleids) : expired_context {
|
||||
$this->set_roleids_for('expiredroles', $roleids);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of expired role IDs.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_expiredroles() {
|
||||
return $this->get_roleids_for('expiredroles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of unexpired role IDs.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function get_unexpiredroles() {
|
||||
return $this->get_roleids_for('unexpiredroles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new expired_context based on the context, and expiry_info object.
|
||||
*
|
||||
* @param \context $context
|
||||
* @param expiry_info $info
|
||||
* @param boolean $save
|
||||
* @return expired_context
|
||||
*/
|
||||
public static function create_from_expiry_info(\context $context, expiry_info $info) : expired_context {
|
||||
public static function create_from_expiry_info(\context $context, expiry_info $info, bool $save = true) : expired_context {
|
||||
$record = (object) [
|
||||
'contextid' => $context->id,
|
||||
'status' => self::STATUS_EXPIRED,
|
||||
'defaultexpired' => (int) $info->is_default_expired(),
|
||||
];
|
||||
|
||||
$expiredcontext = new static(0, $record);
|
||||
$expiredcontext->save();
|
||||
$expiredcontext->set('expiredroles', $info->get_expired_roles());
|
||||
$expiredcontext->set('unexpiredroles', $info->get_unexpired_roles());
|
||||
|
||||
if ($save) {
|
||||
$expiredcontext->save();
|
||||
}
|
||||
|
||||
return $expiredcontext;
|
||||
}
|
||||
@@ -186,7 +310,42 @@ class expired_context extends \core\persistent {
|
||||
* @return $this
|
||||
*/
|
||||
public function update_from_expiry_info(expiry_info $info) : expired_context {
|
||||
$save = false;
|
||||
|
||||
// Compare the expiredroles.
|
||||
$thisexpired = $this->get('expiredroles');
|
||||
$infoexpired = $info->get_expired_roles();
|
||||
|
||||
sort($thisexpired);
|
||||
sort($infoexpired);
|
||||
if ($infoexpired != $thisexpired) {
|
||||
$this->set('expiredroles', $infoexpired);
|
||||
$save = true;
|
||||
}
|
||||
|
||||
// Compare the unexpiredroles.
|
||||
$thisunexpired = $this->get('unexpiredroles');
|
||||
$infounexpired = $info->get_unexpired_roles();
|
||||
|
||||
sort($thisunexpired);
|
||||
sort($infounexpired);
|
||||
if ($infounexpired != $thisunexpired) {
|
||||
$this->set('unexpiredroles', $infounexpired);
|
||||
$save = true;
|
||||
}
|
||||
|
||||
if (empty($this->get('defaultexpired')) == $info->is_default_expired()) {
|
||||
$this->set('defaultexpired', (int) $info->is_default_expired());
|
||||
$save = true;
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$this->set('status', self::STATUS_EXPIRED);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,4 +365,14 @@ class expired_context extends \core\persistent {
|
||||
public function is_complete() : bool {
|
||||
return ($this->get('status') == self::STATUS_CLEANED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this context has 'fully' expired.
|
||||
* That is to say that the default retention period has been reached, and that there are no unexpired roles.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_fully_expired() : bool {
|
||||
return $this->get('defaultexpired') && empty($this->get('unexpiredroles'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,34 +47,62 @@ class expired_contexts_manager {
|
||||
/** @var manager The privacy manager */
|
||||
protected $manager = null;
|
||||
|
||||
/** @var \progress_trace Trace tool for logging */
|
||||
protected $trace = null;
|
||||
|
||||
/**
|
||||
* Constructor for the expired_contexts_manager.
|
||||
*
|
||||
* @param \progress_trace $trace
|
||||
*/
|
||||
public function __construct(\progress_trace $trace = null) {
|
||||
if (null === $trace) {
|
||||
$trace = new \null_progress_trace();
|
||||
}
|
||||
|
||||
$this->trace = $trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag expired contexts as expired.
|
||||
*
|
||||
* @return int[] The number of contexts flagged as expired for courses, and users.
|
||||
*/
|
||||
public function flag_expired_contexts() : array {
|
||||
$this->trace->output('Checking requirements');
|
||||
if (!$this->check_requirements()) {
|
||||
$this->trace->output('Requirements not met. Cannot process expired retentions.', 1);
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
// Clear old and stale records first.
|
||||
$this->trace->output('Clearing obselete records.', 0);
|
||||
static::clear_old_records();
|
||||
$this->trace->output('Done.', 1);
|
||||
|
||||
$this->trace->output('Calculating potential course expiries.', 0);
|
||||
$data = static::get_nested_expiry_info_for_courses();
|
||||
|
||||
$coursecount = 0;
|
||||
$this->trace->output('Updating course expiry data.', 0);
|
||||
foreach ($data as $expiryrecord) {
|
||||
if ($this->update_from_expiry_info($expiryrecord)) {
|
||||
$coursecount++;
|
||||
}
|
||||
}
|
||||
$this->trace->output('Done.', 1);
|
||||
|
||||
$this->trace->output('Calculating potential user expiries.', 0);
|
||||
$data = static::get_nested_expiry_info_for_user();
|
||||
|
||||
$usercount = 0;
|
||||
$this->trace->output('Updating user expiry data.', 0);
|
||||
foreach ($data as $expiryrecord) {
|
||||
if ($this->update_from_expiry_info($expiryrecord)) {
|
||||
$usercount++;
|
||||
}
|
||||
}
|
||||
$this->trace->output('Done.', 1);
|
||||
|
||||
return [$coursecount, $usercount];
|
||||
}
|
||||
@@ -241,6 +269,8 @@ class expired_contexts_manager {
|
||||
$datalist = [];
|
||||
$expiredcontents = [];
|
||||
$pathstoskip = [];
|
||||
|
||||
$userpurpose = data_registry::get_effective_contextlevel_value(CONTEXT_USER, 'purpose');
|
||||
foreach ($fulllist as $record) {
|
||||
\context_helper::preload_from_record($record);
|
||||
$context = \context::instance_by_id($record->id, false);
|
||||
@@ -263,14 +293,19 @@ class expired_contexts_manager {
|
||||
continue;
|
||||
}
|
||||
|
||||
$purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
|
||||
$purpose = api::get_effective_context_purpose($context, $purposevalue);
|
||||
if ($context instanceof \context_user) {
|
||||
$purpose = $userpurpose;
|
||||
} else {
|
||||
$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) {
|
||||
@@ -279,6 +314,7 @@ class expired_contexts_manager {
|
||||
|
||||
$expiryinfo->merge_with_child($data->info);
|
||||
}
|
||||
|
||||
$datalist[$context->path] = (object) [
|
||||
'context' => $context,
|
||||
'record' => $record,
|
||||
@@ -309,44 +345,7 @@ class expired_contexts_manager {
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
$shouldskip = !self::are_user_context_dependencies_expired($context);
|
||||
}
|
||||
|
||||
if ($shouldskip) {
|
||||
@@ -363,16 +362,21 @@ class expired_contexts_manager {
|
||||
* @return int[] The number of deleted contexts.
|
||||
*/
|
||||
public function process_approved_deletions() : array {
|
||||
$this->trace->output('Checking requirements');
|
||||
if (!$this->check_requirements()) {
|
||||
$this->trace->output('Requirements not met. Cannot process expired retentions.', 1);
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
$this->trace->output('Fetching all approved and expired contexts for deletion.');
|
||||
$expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]);
|
||||
$this->trace->output('Done.', 1);
|
||||
$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.
|
||||
@@ -380,7 +384,9 @@ class expired_contexts_manager {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->trace->output("Deleting data for " . $context->get_context_name(), 2);
|
||||
if ($this->delete_expired_context($expiredctx)) {
|
||||
$this->trace->output("Done.", 3);
|
||||
if ($context instanceof \context_user) {
|
||||
$usercount++;
|
||||
} else {
|
||||
@@ -425,11 +431,39 @@ class expired_contexts_manager {
|
||||
}
|
||||
|
||||
$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);
|
||||
if ($expiredctx->is_fully_expired()) {
|
||||
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, and there are
|
||||
// no remaining overrides.
|
||||
$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;
|
||||
}
|
||||
|
||||
// We need to find all users in the context, and delete just those who have expired.
|
||||
$collection = $privacymanager->get_users_in_context($context);
|
||||
|
||||
// Apply the expired and unexpired filters to remove the users in these categories.
|
||||
$userassignments = $this->get_role_users_for_expired_context($expiredctx, $context);
|
||||
$approvedcollection = new \core_privacy\local\request\userlist_collection($context);
|
||||
foreach ($collection as $pendinguserlist) {
|
||||
$userlist = filtered_userlist::create_from_userlist($pendinguserlist);
|
||||
$userlist->apply_expired_context_filters($userassignments->expired, $userassignments->unexpired);
|
||||
if (count($userlist)) {
|
||||
$approvedcollection->add_userlist($userlist);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($approvedcollection)) {
|
||||
// Perform the deletion with the newly approved collection.
|
||||
$privacymanager->delete_data_for_users_in_context($approvedcollection);
|
||||
}
|
||||
|
||||
// Mark the record as cleaned.
|
||||
@@ -545,14 +579,45 @@ class expired_contexts_manager {
|
||||
* @return expiry_info
|
||||
*/
|
||||
protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0) : expiry_info {
|
||||
if (empty($comparisondate)) {
|
||||
// The date is empty, therefore this context cannot be considered for automatic expiry.
|
||||
$defaultexpired = false;
|
||||
} else {
|
||||
$defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
|
||||
}
|
||||
$overrides = $purpose->get_purpose_overrides();
|
||||
$expiredroles = $unexpiredroles = [];
|
||||
if (empty($overrides)) {
|
||||
// There are no overrides for this purpose.
|
||||
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);
|
||||
return new expiry_info($defaultexpired, $purpose->get('protected'), [], [], []);
|
||||
} else {
|
||||
$protectedroles = [];
|
||||
foreach ($overrides as $override) {
|
||||
if (static::has_expired($override->get('retentionperiod'), $comparisondate)) {
|
||||
// This role has expired.
|
||||
$expiredroles[] = $override->get('roleid');
|
||||
} else {
|
||||
// This role has not yet expired.
|
||||
$unexpiredroles[] = $override->get('roleid');
|
||||
|
||||
if ($override->get('protected')) {
|
||||
$protectedroles[$override->get('roleid')] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$defaultexpired = false;
|
||||
if (static::has_expired($purpose->get('retentionperiod'), $comparisondate)) {
|
||||
$defaultexpired = true;
|
||||
}
|
||||
|
||||
if ($defaultexpired) {
|
||||
$expiredroles = [];
|
||||
}
|
||||
|
||||
return new expiry_info($defaultexpired, $purpose->get('protected'), $expiredroles, $unexpiredroles, $protectedroles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -565,7 +630,7 @@ class expired_contexts_manager {
|
||||
* @return expired_context|null
|
||||
*/
|
||||
protected function update_from_expiry_info(\stdClass $expiryrecord) {
|
||||
if ($expiryrecord->info->is_any_expired()) {
|
||||
if ($isanyexpired = $expiryrecord->info->is_any_expired()) {
|
||||
// The context is expired in some fashion.
|
||||
// Create or update as required.
|
||||
if ($expiryrecord->record->expiredctxid) {
|
||||
@@ -579,6 +644,15 @@ class expired_contexts_manager {
|
||||
$expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);
|
||||
}
|
||||
|
||||
if ($expiryrecord->context instanceof \context_user) {
|
||||
$userassignments = $this->get_role_users_for_expired_context($expiredcontext, $expiryrecord->context);
|
||||
if (!empty($userassignments->unexpired)) {
|
||||
$expiredcontext->delete();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $expiredcontext;
|
||||
} else {
|
||||
// The context is not expired.
|
||||
@@ -608,7 +682,6 @@ class expired_contexts_manager {
|
||||
// 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;
|
||||
@@ -650,6 +723,80 @@ class expired_contexts_manager {
|
||||
return $expiredctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of actual users for the combination of expired, and unexpired roles.
|
||||
*
|
||||
* @param expired_context $expiredctx
|
||||
* @param \context $context
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_role_users_for_expired_context(expired_context $expiredctx, \context $context) : \stdClass {
|
||||
$expiredroles = $expiredctx->get('expiredroles');
|
||||
$expiredroleusers = [];
|
||||
if (!empty($expiredroles)) {
|
||||
// Find the list of expired role users.
|
||||
$expiredroleuserassignments = get_role_users($expiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id');
|
||||
$expiredroleusers = array_map(function($assignment) {
|
||||
return $assignment->userid;
|
||||
}, $expiredroleuserassignments);
|
||||
}
|
||||
$expiredroleusers = array_unique($expiredroleusers);
|
||||
|
||||
$unexpiredroles = $expiredctx->get('unexpiredroles');
|
||||
$unexpiredroleusers = [];
|
||||
if (!empty($unexpiredroles)) {
|
||||
// Find the list of unexpired role users.
|
||||
$unexpiredroleuserassignments = get_role_users($unexpiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id');
|
||||
$unexpiredroleusers = array_map(function($assignment) {
|
||||
return $assignment->userid;
|
||||
}, $unexpiredroleuserassignments);
|
||||
}
|
||||
$unexpiredroleusers = array_unique($unexpiredroleusers);
|
||||
|
||||
if (!$expiredctx->get('defaultexpired')) {
|
||||
$tofilter = get_users_roles($context, $expiredroleusers);
|
||||
$tofilter = array_filter($tofilter, function($userroles) use ($expiredroles) {
|
||||
// Each iteration contains the list of role assignment for a specific user.
|
||||
// All roles that the user holds must match those in the list of expired roles.
|
||||
foreach ($userroles as $ra) {
|
||||
if (false === array_search($ra->roleid, $expiredroles)) {
|
||||
// This role was not found in the list of assignments.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
$unexpiredroleusers = array_merge($unexpiredroleusers, array_keys($tofilter));
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'expired' => $expiredroleusers,
|
||||
'unexpired' => $unexpiredroleusers,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the supplied context has expired.
|
||||
*
|
||||
* @param \context $context
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_context_expired(\context $context) : bool {
|
||||
$parents = $context->get_parent_contexts(true);
|
||||
foreach ($parents as $parent) {
|
||||
if ($parent instanceof \context_course) {
|
||||
return self::is_course_context_expired($context);
|
||||
}
|
||||
|
||||
if ($parent instanceof \context_user) {
|
||||
return self::are_user_context_dependencies_expired($context);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the course has expired.
|
||||
*
|
||||
@@ -658,11 +805,149 @@ class expired_contexts_manager {
|
||||
*/
|
||||
protected static function is_course_expired(\stdClass $course) : bool {
|
||||
$context = \context_course::instance($course->id);
|
||||
|
||||
return self::is_course_context_expired($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the supplied course context has expired.
|
||||
*
|
||||
* @param \context_course $context
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_course_context_expired(\context_course $context) : bool {
|
||||
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
|
||||
|
||||
return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the supplied user context's dependencies have expired.
|
||||
*
|
||||
* This checks whether courses have expired, and some other check, but does not check whether the user themself has expired.
|
||||
*
|
||||
* Although this seems unusual at first, each location calling this actually checks whether the user is elgible for
|
||||
* deletion, irrespective if they have actually expired.
|
||||
*
|
||||
* For example, a request to delete the user only cares about course dependencies and the user's lack of expiry
|
||||
* should not block their own request to be deleted; whilst the expiry eligibility check has already tested for the
|
||||
* user being expired.
|
||||
*
|
||||
* @param \context_user $context
|
||||
* @return bool
|
||||
*/
|
||||
protected static function are_user_context_dependencies_expired(\context_user $context) : bool {
|
||||
// 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 expire.
|
||||
return false;
|
||||
}
|
||||
|
||||
$courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
|
||||
$requireenddate = self::require_all_end_dates_for_user_deletion();
|
||||
|
||||
$expired = true;
|
||||
|
||||
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.
|
||||
$expired = false;
|
||||
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.
|
||||
$expired = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// This course has an end date which is in the past.
|
||||
if (!self::is_course_expired($course)) {
|
||||
// This course has not expired yet.
|
||||
$expired = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $expired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the supplied context has expired or unprotected for the specified user.
|
||||
*
|
||||
* @param \context $context
|
||||
* @param \stdClass $user
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) : bool {
|
||||
$parents = $context->get_parent_contexts(true);
|
||||
foreach ($parents as $parent) {
|
||||
if ($parent instanceof \context_course) {
|
||||
return self::is_course_context_expired_or_unprotected_for_user($parent, $user);
|
||||
}
|
||||
|
||||
if ($parent instanceof \context_user) {
|
||||
return self::are_user_context_dependencies_expired($context);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the supplied course context has expired, or is unprotected.
|
||||
*
|
||||
* @param \context_course $context
|
||||
* @param \stdClass $user
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_course_context_expired_or_unprotected_for_user(\context_course $context, \stdClass $user) {
|
||||
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
|
||||
|
||||
$info = $expiryrecords[$context->path]->info;
|
||||
if ($info->is_fully_expired()) {
|
||||
// This context is fully expired.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Now perform user checks.
|
||||
$userroles = array_map(function($assignment) {
|
||||
return $assignment->roleid;
|
||||
}, get_user_roles($context, $user->id));
|
||||
|
||||
$unexpiredprotectedroles = $info->get_unexpired_protected_roles();
|
||||
if (!empty(array_intersect($unexpiredprotectedroles, $userroles))) {
|
||||
// The user holds an unexpired and protected role.
|
||||
return false;
|
||||
}
|
||||
|
||||
$unprotectedoverriddenroles = $info->get_unprotected_overridden_roles();
|
||||
$matchingroles = array_intersect($unprotectedoverriddenroles, $userroles);
|
||||
if (!empty($matchingroles)) {
|
||||
// This user has at least one overridden role which is not a protected.
|
||||
// However, All such roles must match.
|
||||
// If the user has multiple roles then all must be expired, otherwise we should fall back to the default behaviour.
|
||||
if (empty(array_diff($userroles, $unprotectedoverriddenroles))) {
|
||||
// All roles that this user holds are a combination of expired, or unprotected.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($info->is_default_expired()) {
|
||||
// If the user has no unexpired roles, and the context is expired by default then this must be expired.
|
||||
return true;
|
||||
}
|
||||
|
||||
return !$info->is_default_protected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the privacy manager.
|
||||
*
|
||||
|
||||
@@ -36,15 +36,38 @@ defined('MOODLE_INTERNAL') || die();
|
||||
class expiry_info {
|
||||
|
||||
/** @var bool Whether this context is fully expired */
|
||||
protected $isexpired = false;
|
||||
protected $fullyexpired = false;
|
||||
|
||||
/** @var bool Whether the default expiry value of this purpose has been reached */
|
||||
protected $defaultexpiryreached = false;
|
||||
|
||||
/** @var bool Whether the default purpose is protected */
|
||||
protected $defaultprotected = false;
|
||||
|
||||
/** @var int[] List of expires roles */
|
||||
protected $expired = [];
|
||||
|
||||
/** @var int[] List of unexpires roles */
|
||||
protected $unexpired = [];
|
||||
|
||||
/** @var int[] List of unexpired roles which are also protected */
|
||||
protected $protectedroles = [];
|
||||
|
||||
/**
|
||||
* Constructor for the expiry_info class.
|
||||
*
|
||||
* @param bool $isexpired Whether the retention period for this context has expired yet.
|
||||
* @param bool $default Whether the default expiry period for this context has been reached.
|
||||
* @param bool $defaultprotected Whether the default expiry is protected.
|
||||
* @param int[] $expired A list of roles in this context which have explicitly expired.
|
||||
* @param int[] $unexpired A list of roles in this context which have not yet expired.
|
||||
* @param int[] $protectedroles A list of unexpired roles in this context which are protected.
|
||||
*/
|
||||
public function __construct(bool $isexpired) {
|
||||
$this->isexpired = $isexpired;
|
||||
public function __construct(bool $default, bool $defaultprotected, array $expired, array $unexpired, array $protectedroles) {
|
||||
$this->defaultexpiryreached = $default;
|
||||
$this->defaultprotected = $defaultprotected;
|
||||
$this->expired = $expired;
|
||||
$this->unexpired = $unexpired;
|
||||
$this->protectedroles = $protectedroles;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +77,7 @@ class expiry_info {
|
||||
* @return bool
|
||||
*/
|
||||
public function is_fully_expired() : bool {
|
||||
return $this->isexpired;
|
||||
return $this->defaultexpiryreached && empty($this->unexpired);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,9 +90,87 @@ class expiry_info {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($this->get_expired_roles())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->is_default_expired()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of explicitly expired role IDs.
|
||||
* Note: This does not list roles which have been expired via the default retention policy being reached.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_expired_roles() : array {
|
||||
if ($this->is_default_expired()) {
|
||||
return [];
|
||||
}
|
||||
return $this->expired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the specified role is explicitly expired.
|
||||
* Note: This does not list roles which have been expired via the default retention policy being reached.
|
||||
*
|
||||
* @param int $roleid
|
||||
* @return bool
|
||||
*/
|
||||
public function is_role_expired(int $roleid) : bool {
|
||||
return false !== array_search($roleid, $this->expired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default retention policy has been reached.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_default_expired() : bool {
|
||||
return $this->defaultexpiryreached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the default purpose is protected.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_default_protected() : bool {
|
||||
return $this->defaultprotected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of unexpired role IDs.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_unexpired_roles() : array {
|
||||
return $this->unexpired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of unexpired protected roles.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_unexpired_protected_roles() : array {
|
||||
return array_keys(array_filter($this->protectedroles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all overridden roles which are unprotected.
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_unprotected_overridden_roles() : array {
|
||||
$allroles = array_merge($this->expired, $this->unexpired);
|
||||
|
||||
return array_diff($allroles, $this->protectedroles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
|
||||
*
|
||||
@@ -86,7 +187,20 @@ class expiry_info {
|
||||
}
|
||||
|
||||
// If the child is not fully expired, then none of the parents can be either.
|
||||
$this->isexpired = false;
|
||||
$this->fullyexpired = false;
|
||||
|
||||
// Remove any role in this node which is not expired in the child.
|
||||
foreach ($this->expired as $key => $roleid) {
|
||||
if (!$child->is_role_expired($roleid)) {
|
||||
unset($this->expired[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
array_merge($this->unexpired, $child->get_unexpired_roles());
|
||||
|
||||
if (!$child->is_default_expired()) {
|
||||
$this->defaultexpiryreached = false;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use coding_exception;
|
||||
use core\external\persistent_exporter;
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
use renderer_base;
|
||||
use tool_dataprivacy\context_instance;
|
||||
@@ -79,6 +78,9 @@ class purpose_exporter extends persistent_exporter {
|
||||
'multiple' => true,
|
||||
'optional' => true
|
||||
],
|
||||
'roleoverrides' => [
|
||||
'type' => PARAM_TEXT
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -125,23 +127,14 @@ class purpose_exporter extends persistent_exporter {
|
||||
|
||||
$retentionperiod = $this->persistent->get('retentionperiod');
|
||||
if ($retentionperiod) {
|
||||
$interval = new DateInterval($retentionperiod);
|
||||
|
||||
// It is one or another.
|
||||
if ($interval->y) {
|
||||
$formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
|
||||
} else if ($interval->m) {
|
||||
$formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
|
||||
} else if ($interval->d) {
|
||||
$formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
|
||||
} else {
|
||||
$formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
|
||||
}
|
||||
$formattedtime = \tool_dataprivacy\api::format_retention_period(new \DateInterval($retentionperiod));
|
||||
} else {
|
||||
$formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy');
|
||||
}
|
||||
$values['formattedretentionperiod'] = $formattedtime;
|
||||
|
||||
$values['roleoverrides'] = !empty($this->persistent->get_purpose_overrides());
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* An implementation of a userlist which has been filtered and approved.
|
||||
*
|
||||
* @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;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* An implementation of a userlist which can be filtered by role.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class filtered_userlist extends \core_privacy\local\request\approved_userlist {
|
||||
|
||||
/**
|
||||
* Apply filters to only remove users in the expireduserids list, and to remove any who are in the unexpired list.
|
||||
* The unexpired list wins where a user is in both lists.
|
||||
*
|
||||
* @param int[] $expireduserids The list of userids for users who should be expired.
|
||||
* @param int[] $unexpireduserids The list of userids for those users who should not be expired.
|
||||
* @return $this
|
||||
*/
|
||||
public function apply_expired_context_filters(array $expireduserids, array $unexpireduserids) : filtered_userlist {
|
||||
// The current userlist content.
|
||||
$userids = $this->get_userids();
|
||||
|
||||
if (!empty($expireduserids)) {
|
||||
// Now remove any not on the list of expired users.
|
||||
$userids = array_intersect($userids, $expireduserids);
|
||||
}
|
||||
|
||||
if (!empty($unexpireduserids)) {
|
||||
// Remove any on the list of unexpiredusers users.
|
||||
$userids = array_diff($userids, $unexpireduserids);
|
||||
}
|
||||
|
||||
$this->set_userids($userids);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,11 @@ class purpose extends persistent {
|
||||
*/
|
||||
protected static $persistentclass = 'tool_dataprivacy\\purpose';
|
||||
|
||||
/**
|
||||
* @var array The list of current overrides.
|
||||
*/
|
||||
protected $existingoverrides = [];
|
||||
|
||||
/**
|
||||
* Define the form - called by parent constructor
|
||||
*/
|
||||
@@ -56,41 +61,16 @@ class purpose extends persistent {
|
||||
$mform->setType('description', PARAM_CLEANHTML);
|
||||
|
||||
// Field for selecting lawful bases (from GDPR Article 6.1).
|
||||
$lawfulbases = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
|
||||
$key = 'gdpr_art_6_1_' . $article;
|
||||
$lawfulbases[$key] = get_string($key . '_name', 'tool_dataprivacy');
|
||||
}
|
||||
$options = array(
|
||||
'multiple' => true,
|
||||
);
|
||||
$mform->addElement('autocomplete', 'lawfulbases', get_string('lawfulbases', 'tool_dataprivacy'), $lawfulbases, $options);
|
||||
$this->add_field($this->get_lawful_base_field());
|
||||
$mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
|
||||
$mform->addHelpButton('lawfulbases', 'lawfulbases', 'tool_dataprivacy');
|
||||
|
||||
// Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
|
||||
$sensitivereasons = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
|
||||
$key = 'gdpr_art_9_2_' . $article;
|
||||
$sensitivereasons[$key] = get_string($key . '_name', 'tool_dataprivacy');
|
||||
}
|
||||
$mform->addElement('autocomplete', 'sensitivedatareasons', get_string('sensitivedatareasons', 'tool_dataprivacy'),
|
||||
$sensitivereasons, $options);
|
||||
$mform->addHelpButton('sensitivedatareasons', 'sensitivedatareasons', 'tool_dataprivacy');
|
||||
$this->add_field($this->get_sensitive_base_field());
|
||||
|
||||
$number = $mform->createElement('text', 'retentionperiodnumber', null, ['size' => 8]);
|
||||
$unitoptions = [
|
||||
'Y' => get_string('years'),
|
||||
'M' => strtolower(get_string('months')),
|
||||
'D' => strtolower(get_string('days'))
|
||||
];
|
||||
$unit = $mform->createElement('select', 'retentionperiodunit', '', $unitoptions);
|
||||
$mform->addGroup(['number' => $number, 'unit' => $unit], 'retentionperiod',
|
||||
get_string('retentionperiod', 'tool_dataprivacy'), null, false);
|
||||
$mform->setType('retentionperiodnumber', PARAM_INT);
|
||||
$this->add_field($this->get_retention_period_fields());
|
||||
$this->add_field($this->get_protected_field());
|
||||
|
||||
$this->_form->addElement('advcheckbox', 'protected', get_string('protected', 'tool_dataprivacy'),
|
||||
get_string('protectedlabel', 'tool_dataprivacy'));
|
||||
$this->add_override_fields();
|
||||
|
||||
if (!empty($this->_customdata['showbuttons'])) {
|
||||
if (!$this->get_persistent()->get('id')) {
|
||||
@@ -102,16 +82,360 @@ class purpose extends persistent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a fieldset to the current form.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
*/
|
||||
protected function add_field(\stdClass $data) {
|
||||
foreach ($data->fields as $field) {
|
||||
$this->_form->addElement($field);
|
||||
}
|
||||
|
||||
if (!empty($data->helps)) {
|
||||
foreach ($data->helps as $fieldname => $helpdata) {
|
||||
$help = array_merge([$fieldname], $helpdata);
|
||||
call_user_func_array([$this->_form, 'addHelpButton'], $help);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data->types)) {
|
||||
foreach ($data->types as $fieldname => $type) {
|
||||
$this->_form->setType($fieldname, $type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data->rules)) {
|
||||
foreach ($data->rules as $fieldname => $ruledata) {
|
||||
$rule = array_merge([$fieldname], $ruledata);
|
||||
call_user_func_array([$this->_form, 'addRule'], $rule);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data->defaults)) {
|
||||
foreach ($data->defaults as $fieldname => $default) {
|
||||
$this->_form($fieldname, $default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle addition of relevant repeated element fields for role overrides.
|
||||
*/
|
||||
protected function add_override_fields() {
|
||||
$purpose = $this->get_persistent();
|
||||
|
||||
if (empty($purpose->get('id'))) {
|
||||
// It is not possible to use repeated elements in a modal form yet.
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = [
|
||||
$this->get_role_override_id('roleoverride_'),
|
||||
$this->get_role_field('roleoverride_'),
|
||||
$this->get_retention_period_fields('roleoverride_'),
|
||||
$this->get_protected_field('roleoverride_'),
|
||||
$this->get_lawful_base_field('roleoverride_'),
|
||||
$this->get_sensitive_base_field('roleoverride_'),
|
||||
];
|
||||
|
||||
$options = [
|
||||
'type' => [],
|
||||
'helpbutton' => [],
|
||||
];
|
||||
|
||||
// Start by adding the title.
|
||||
$overrideelements = [
|
||||
$this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')),
|
||||
$this->_form->createElement(
|
||||
'static',
|
||||
'roleoverrideoverview',
|
||||
'',
|
||||
get_string('roleoverrideoverview', 'tool_dataprivacy')
|
||||
),
|
||||
];
|
||||
|
||||
foreach ($fields as $fielddata) {
|
||||
foreach ($fielddata->fields as $field) {
|
||||
$overrideelements[] = $field;
|
||||
}
|
||||
|
||||
if (!empty($fielddata->helps)) {
|
||||
foreach ($fielddata->helps as $name => $help) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['helpbutton'] = $help;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->types)) {
|
||||
foreach ($fielddata->types as $name => $type) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['type'] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->rules)) {
|
||||
foreach ($fielddata->rules as $name => $rule) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['rule'] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->defaults)) {
|
||||
foreach ($fielddata->defaults as $name => $default) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['default'] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fielddata->advanceds)) {
|
||||
foreach ($fielddata->advanceds as $name => $advanced) {
|
||||
if (!isset($options[$name])) {
|
||||
$options[$name] = [];
|
||||
}
|
||||
$options[$name]['advanced'] = $advanced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->existingoverrides = $purpose->get_purpose_overrides();
|
||||
$existingoverridecount = count($this->existingoverrides);
|
||||
|
||||
$this->repeat_elements(
|
||||
$overrideelements,
|
||||
$existingoverridecount,
|
||||
$options,
|
||||
'overrides',
|
||||
'addoverride',
|
||||
1,
|
||||
get_string('addroleoverride', 'tool_dataprivacy')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fields.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function filter_data_for_persistent($data) {
|
||||
$data = parent::filter_data_for_persistent($data);
|
||||
|
||||
$classname = static::$persistentclass;
|
||||
$properties = $classname::properties_definition();
|
||||
|
||||
$data = (object) array_filter((array) $data, function($value, $key) use ($properties) {
|
||||
return isset($properties[$key]);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field for the role name.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_role_override_id(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}id";
|
||||
|
||||
$fielddata = (object) [
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$fielddata->fields[] = $this->_form->createElement('hidden', $fieldname);
|
||||
$fielddata->types[$fieldname] = PARAM_INT;
|
||||
|
||||
return $fielddata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field for the role name.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_role_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}roleid";
|
||||
|
||||
$fielddata = (object) [
|
||||
'fields' => [],
|
||||
'helps' => [],
|
||||
];
|
||||
|
||||
$roles = [
|
||||
'' => get_string('none'),
|
||||
];
|
||||
foreach (role_get_names() as $roleid => $role) {
|
||||
$roles[$roleid] = $role->localname;
|
||||
}
|
||||
|
||||
$fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'),
|
||||
$roles,
|
||||
[
|
||||
'multiple' => false,
|
||||
]
|
||||
);
|
||||
$fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy'];
|
||||
$fielddata->defaults[$fieldname] = null;
|
||||
|
||||
return $fielddata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mform field for lawful bases.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_lawful_base_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}lawfulbases";
|
||||
|
||||
$data = (object) [
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$bases = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
|
||||
$key = 'gdpr_art_6_1_' . $article;
|
||||
$bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'),
|
||||
$bases,
|
||||
[
|
||||
'multiple' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$data->helps = [
|
||||
$fieldname => ['lawfulbases', 'tool_dataprivacy'],
|
||||
];
|
||||
|
||||
$data->advanceds = [
|
||||
$fieldname => true,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mform field for sensitive bases.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_sensitive_base_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}sensitivedatareasons";
|
||||
|
||||
$data = (object) [
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$bases = [];
|
||||
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
|
||||
$key = 'gdpr_art_9_2_' . $article;
|
||||
$bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$data->fields[] = $this->_form->createElement(
|
||||
'autocomplete',
|
||||
$fieldname,
|
||||
get_string('sensitivedatareasons', 'tool_dataprivacy'),
|
||||
$bases,
|
||||
[
|
||||
'multiple' => true,
|
||||
]
|
||||
);
|
||||
$data->helps = [
|
||||
$fieldname => ['sensitivedatareasons', 'tool_dataprivacy'],
|
||||
];
|
||||
|
||||
$data->advanceds = [
|
||||
$fieldname => true,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the retention period fields.
|
||||
*
|
||||
* @param string $prefix The name of the main field, and prefix for the subfields.
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_retention_period_fields(string $prefix = '') : \stdClass {
|
||||
$prefix = "{$prefix}retentionperiod";
|
||||
$data = (object) [
|
||||
'fields' => [],
|
||||
'types' => [],
|
||||
];
|
||||
|
||||
$number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]);
|
||||
$data->types["{$prefix}number"] = PARAM_INT;
|
||||
|
||||
$unitoptions = [
|
||||
'Y' => get_string('years'),
|
||||
'M' => strtolower(get_string('months')),
|
||||
'D' => strtolower(get_string('days'))
|
||||
];
|
||||
$unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions);
|
||||
|
||||
$data->fields[] = $this->_form->createElement(
|
||||
'group',
|
||||
$prefix,
|
||||
get_string('retentionperiod', 'tool_dataprivacy'),
|
||||
[
|
||||
'number' => $number,
|
||||
'unit' => $unit,
|
||||
],
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mform field for the protected flag.
|
||||
*
|
||||
* @param string $prefix The prefix to apply to the field
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function get_protected_field(string $prefix = '') : \stdClass {
|
||||
$fieldname = "{$prefix}protected";
|
||||
|
||||
return (object) [
|
||||
'fields' => [
|
||||
$this->_form->createElement(
|
||||
'advcheckbox',
|
||||
$fieldname,
|
||||
get_string('protected', 'tool_dataprivacy'),
|
||||
get_string('protectedlabel', 'tool_dataprivacy')
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts data to data suitable for storage.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected static function convert_fields(\stdClass $data) {
|
||||
$data = parent::convert_fields($data);
|
||||
|
||||
if (is_array($data->lawfulbases)) {
|
||||
if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) {
|
||||
$data->lawfulbases = implode(',', $data->lawfulbases);
|
||||
}
|
||||
if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
|
||||
@@ -122,6 +446,7 @@ class purpose extends persistent {
|
||||
$data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit;
|
||||
unset($data->retentionperiodnumber);
|
||||
unset($data->retentionperiodunit);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -133,6 +458,16 @@ class purpose extends persistent {
|
||||
protected function get_default_data() {
|
||||
$data = parent::get_default_data();
|
||||
|
||||
return $this->convert_existing_data_to_values($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise any values stored in existing data.
|
||||
*
|
||||
* @param \stdClass $data
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function convert_existing_data_to_values(\stdClass $data) : \stdClass {
|
||||
$data->lawfulbases = explode(',', $data->lawfulbases);
|
||||
if (!empty($data->sensitivedatareasons)) {
|
||||
$data->sensitivedatareasons = explode(',', $data->sensitivedatareasons);
|
||||
@@ -146,4 +481,94 @@ class purpose extends persistent {
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the role override data from the list of submitted data.
|
||||
*
|
||||
* @param \stdClass $data The complete set of processed data
|
||||
* @return \stdClass[] The list of overrides
|
||||
*/
|
||||
public function get_role_overrides_from_data(\stdClass $data) {
|
||||
$overrides = [];
|
||||
if (!empty($data->overrides)) {
|
||||
$searchkey = 'roleoverride_';
|
||||
|
||||
for ($i = 0; $i < $data->overrides; $i++) {
|
||||
$overridedata = (object) [];
|
||||
foreach ((array) $data as $fieldname => $value) {
|
||||
if (strpos($fieldname, $searchkey) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$overridefieldname = substr($fieldname, strlen($searchkey));
|
||||
$overridedata->$overridefieldname = $value[$i];
|
||||
}
|
||||
|
||||
if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) {
|
||||
// Skip this one.
|
||||
// There is no value and it will be delete.
|
||||
continue;
|
||||
}
|
||||
|
||||
$override = static::convert_fields($overridedata);
|
||||
|
||||
$overrides[$i] = $override;
|
||||
}
|
||||
}
|
||||
|
||||
return $overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define extra validation mechanims.
|
||||
*
|
||||
* @param stdClass $data Data to validate.
|
||||
* @param array $files Array of files.
|
||||
* @param array $errors Currently reported errors.
|
||||
* @return array of additional errors, or overridden errors.
|
||||
*/
|
||||
protected function extra_validation($data, $files, array &$errors) {
|
||||
$overrides = $this->get_role_overrides_from_data($data);
|
||||
|
||||
// Check role overrides to ensure that:
|
||||
// - roles are unique; and
|
||||
// - specifeid retention periods are numeric.
|
||||
$seenroleids = [];
|
||||
foreach ($overrides as $id => $override) {
|
||||
$override->purposeid = 0;
|
||||
$persistent = new \tool_dataprivacy\purpose_override($override->id, $override);
|
||||
|
||||
if (isset($seenroleids[$persistent->get('roleid')])) {
|
||||
$errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole');
|
||||
}
|
||||
$seenroleids[$persistent->get('roleid')] = true;
|
||||
|
||||
$errors = array_merge($errors, $persistent->get_errors());
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load in existing data as form defaults. Usually new entry defaults are stored directly in
|
||||
* form definition (new entry form); this function is used to load in data where values
|
||||
* already exist and data is being edited (edit entry form).
|
||||
*
|
||||
* @param stdClass $data
|
||||
*/
|
||||
public function set_data($data) {
|
||||
$purpose = $this->get_persistent();
|
||||
|
||||
$count = 0;
|
||||
foreach ($this->existingoverrides as $override) {
|
||||
$overridedata = $this->convert_existing_data_to_values($override->to_record());
|
||||
foreach ($overridedata as $key => $value) {
|
||||
$keyname = "roleoverride_{$key}[{$count}]";
|
||||
$data->$keyname = $value;
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
|
||||
parent::set_data($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,15 @@ class expired_contexts_table extends table_sql {
|
||||
*/
|
||||
protected $selectall = true;
|
||||
|
||||
/** @var purpose[] Array of purposes mapped to the contexts. */
|
||||
/** @var purpose[] Array of purposes by their id. */
|
||||
protected $purposes = [];
|
||||
|
||||
/** @var purpose[] Map of context => purpose. */
|
||||
protected $purposemap = [];
|
||||
|
||||
/** @var array List of roles. */
|
||||
protected $roles = [];
|
||||
|
||||
/**
|
||||
* expired_contexts_table constructor.
|
||||
*
|
||||
@@ -77,6 +83,7 @@ class expired_contexts_table extends table_sql {
|
||||
'purpose' => get_string('purpose', 'tool_dataprivacy'),
|
||||
'category' => get_string('category', 'tool_dataprivacy'),
|
||||
'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'),
|
||||
'tobedeleted' => get_string('tobedeleted', 'tool_dataprivacy'),
|
||||
'timecreated' => get_string('expiry', 'tool_dataprivacy'),
|
||||
];
|
||||
$checkboxattrs = [
|
||||
@@ -93,21 +100,25 @@ class expired_contexts_table extends table_sql {
|
||||
$this->no_sorting('purpose');
|
||||
$this->no_sorting('category');
|
||||
$this->no_sorting('retentionperiod');
|
||||
$this->no_sorting('tobedeleted');
|
||||
|
||||
// Make this table sorted by first name by default.
|
||||
$this->sortable(true, 'timecreated');
|
||||
|
||||
// We use roles in several places.
|
||||
$this->roles = role_get_names();
|
||||
}
|
||||
|
||||
/**
|
||||
* The context name column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function col_name($data) {
|
||||
public function col_name($expiredctx) {
|
||||
global $OUTPUT;
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
|
||||
$parent = $context->get_parent_context();
|
||||
$contextdata = (object)[
|
||||
'name' => $context->get_context_name(false, true),
|
||||
@@ -128,14 +139,14 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The context information column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function col_info($data) {
|
||||
public function col_info($expiredctx) {
|
||||
global $OUTPUT;
|
||||
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
|
||||
|
||||
$children = $context->get_child_contexts();
|
||||
if (empty($children)) {
|
||||
@@ -156,13 +167,13 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The category name column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return mixed
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public function col_category($data) {
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
public function col_category($expiredctx) {
|
||||
$context = context_helper::instance_by_id($expiredctx->get('contextid'));
|
||||
$category = api::get_effective_context_category($context);
|
||||
|
||||
return s($category->get('name'));
|
||||
@@ -171,12 +182,12 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The purpose column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function col_purpose($data) {
|
||||
$purpose = $this->purposes[$data->contextid];
|
||||
public function col_purpose($expiredctx) {
|
||||
$purpose = $this->get_purpose_for_expiry($expiredctx);
|
||||
|
||||
return s($purpose->get('name'));
|
||||
}
|
||||
@@ -184,42 +195,114 @@ class expired_contexts_table extends table_sql {
|
||||
/**
|
||||
* The retention period column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function col_retentionperiod($data) {
|
||||
global $PAGE;
|
||||
public function col_retentionperiod($expiredctx) {
|
||||
$purpose = $this->get_purpose_for_expiry($expiredctx);
|
||||
|
||||
$purpose = $this->purposes[$data->contextid];
|
||||
$expiries = [];
|
||||
|
||||
$exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]);
|
||||
$exportedpurpose = $exporter->export($PAGE->get_renderer('core'));
|
||||
$expiry = html_writer::tag('dt', get_string('default'), ['class' => 'col-sm-3']);
|
||||
if ($expiredctx->get('defaultexpired')) {
|
||||
$expiries[get_string('default')] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
|
||||
]);
|
||||
} else {
|
||||
$expiries[get_string('default')] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
|
||||
]);
|
||||
}
|
||||
|
||||
return $exportedpurpose->formattedretentionperiod;
|
||||
if (!$expiredctx->is_fully_expired()) {
|
||||
$purposeoverrides = $purpose->get_purpose_overrides();
|
||||
|
||||
foreach ($expiredctx->get('unexpiredroles') as $roleid) {
|
||||
$role = $this->roles[$roleid];
|
||||
$override = $purposeoverrides[$roleid];
|
||||
|
||||
$expiries[$role->localname] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($expiredctx->get('expiredroles') as $roleid) {
|
||||
$role = $this->roles[$roleid];
|
||||
$override = $purposeoverrides[$roleid];
|
||||
|
||||
$expiries[$role->localname] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
|
||||
'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$output = array_map(function($rolename, $expiry) {
|
||||
$return = html_writer::tag('dt', $rolename, ['class' => 'col-sm-3']);
|
||||
$return .= html_writer::tag('dd', $expiry, ['class' => 'col-sm-9']);
|
||||
|
||||
return $return;
|
||||
}, array_keys($expiries), $expiries);
|
||||
|
||||
return html_writer::tag('dl', implode($output), ['class' => 'row']);
|
||||
}
|
||||
|
||||
/**
|
||||
* The timecreated a.k.a. the context expiry date column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_timecreated($data) {
|
||||
return userdate($data->timecreated);
|
||||
public function col_timecreated($expiredctx) {
|
||||
return userdate($expiredctx->get('timecreated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the select column.
|
||||
*
|
||||
* @param stdClass $data The row data.
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_select($data) {
|
||||
$id = $data->id;
|
||||
public function col_select($expiredctx) {
|
||||
$id = $expiredctx->get('id');
|
||||
return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatting for the 'tobedeleted' column which indicates in a friendlier fashion whose data will be removed.
|
||||
*
|
||||
* @param stdClass $expiredctx The row data.
|
||||
* @return string
|
||||
*/
|
||||
public function col_tobedeleted($expiredctx) {
|
||||
if ($expiredctx->is_fully_expired()) {
|
||||
return get_string('defaultexpired', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$purpose = $this->get_purpose_for_expiry($expiredctx);
|
||||
|
||||
$a = (object) [];
|
||||
|
||||
$expiredroles = [];
|
||||
foreach ($expiredctx->get('expiredroles') as $roleid) {
|
||||
$expiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
|
||||
}
|
||||
$a->expired = html_writer::tag('ul', implode($expiredroles));
|
||||
|
||||
$unexpiredroles = [];
|
||||
foreach ($expiredctx->get('unexpiredroles') as $roleid) {
|
||||
$unexpiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
|
||||
}
|
||||
$a->unexpired = html_writer::tag('ul', implode($unexpiredroles));
|
||||
|
||||
if ($expiredctx->get('defaultexpired')) {
|
||||
return get_string('defaultexpiredexcept', 'tool_dataprivacy', $a);
|
||||
} else if (empty($unexpiredroles)) {
|
||||
return get_string('defaultunexpired', 'tool_dataprivacy', $a);
|
||||
} else {
|
||||
return get_string('defaultunexpiredwithexceptions', 'tool_dataprivacy', $a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database for results to display in the table.
|
||||
*
|
||||
@@ -241,17 +324,16 @@ class expired_contexts_table extends table_sql {
|
||||
// Only load expired contexts that are awaiting confirmation.
|
||||
$expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED,
|
||||
$sort, $this->get_page_start(), $this->get_page_size());
|
||||
|
||||
$this->rawdata = [];
|
||||
$contextids = [];
|
||||
foreach ($expiredcontexts as $persistent) {
|
||||
$data = $persistent->to_record();
|
||||
|
||||
$context = context_helper::instance_by_id($data->contextid);
|
||||
|
||||
$purpose = api::get_effective_context_purpose($context);
|
||||
$this->purposes[$data->contextid] = $purpose;
|
||||
$this->rawdata[] = $data;
|
||||
$this->rawdata[] = $persistent;
|
||||
$contextids[] = $persistent->get('contextid');
|
||||
}
|
||||
|
||||
$this->preload_contexts($contextids);
|
||||
|
||||
// Set initial bars.
|
||||
if ($useinitialsbar) {
|
||||
$this->initialbars($total > $pagesize);
|
||||
@@ -281,4 +363,48 @@ class expired_contexts_table extends table_sql {
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the purpose for the specified expired context.
|
||||
*
|
||||
* @param expired_context $expiredcontext
|
||||
* @return purpose
|
||||
*/
|
||||
protected function get_purpose_for_expiry(expired_context $expiredcontext) : purpose {
|
||||
$context = context_helper::instance_by_id($expiredcontext->get('contextid'));
|
||||
|
||||
if (empty($this->purposemap[$context->id])) {
|
||||
$purpose = api::get_effective_context_purpose($context);
|
||||
$this->purposemap[$context->id] = $purpose->get('id');
|
||||
|
||||
if (empty($this->purposes[$purpose->get('id')])) {
|
||||
$this->purposes[$purpose->get('id')] = $purpose;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->purposes[$this->purposemap[$context->id]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload context records given a set of contextids.
|
||||
*
|
||||
* @param array $contextids
|
||||
*/
|
||||
protected function preload_contexts(array $contextids) {
|
||||
global $DB;
|
||||
|
||||
if (empty($contextids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
|
||||
$sql = "SELECT {$ctxfields} FROM {context} ctx WHERE ctx.id {$insql}";
|
||||
$contextlist = $DB->get_recordset_sql($sql, $inparams);
|
||||
foreach ($contextlist as $contextdata) {
|
||||
\context_helper::preload_from_record($contextdata);
|
||||
}
|
||||
$contextlist->close();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ use context;
|
||||
use context_user;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use dml_exception;
|
||||
use stdClass;
|
||||
@@ -50,6 +52,9 @@ class provider implements
|
||||
// This tool stores user data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
|
||||
// This tool may provide access to and deletion of user data.
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
@@ -100,6 +105,32 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_user::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_USER,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
$sql = "SELECT instanceid AS userid
|
||||
FROM {context}
|
||||
WHERE id = :contextid
|
||||
AND contextlevel = :contextlevel";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -172,6 +203,15 @@ class provider implements
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user preferences for the plugin.
|
||||
*
|
||||
|
||||
@@ -162,7 +162,6 @@ class purpose extends \core\persistent {
|
||||
* @return null
|
||||
*/
|
||||
public function is_used() {
|
||||
|
||||
if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) ||
|
||||
\tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) {
|
||||
return true;
|
||||
@@ -180,4 +179,13 @@ class purpose extends \core\persistent {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the role purpose overrides for this purpose.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_purpose_overrides() : array {
|
||||
return purpose_override::get_overrides_for_purpose($this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Class for loading/storing data purpose overrides from the DB.
|
||||
*
|
||||
* @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 stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
|
||||
|
||||
/**
|
||||
* Class for loading/storing data purpose overrides from the DB.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class purpose_override extends \core\persistent {
|
||||
|
||||
/**
|
||||
* Database table.
|
||||
*/
|
||||
const TABLE = 'tool_dataprivacy_purposerole';
|
||||
|
||||
/**
|
||||
* Return the definition of the properties of this model.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function define_properties() {
|
||||
return array(
|
||||
'purposeid' => array(
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The purpose that that this override relates to',
|
||||
),
|
||||
'roleid' => array(
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'The role that that this override relates to',
|
||||
),
|
||||
'lawfulbases' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.',
|
||||
'null' => NULL_ALLOWED,
|
||||
'default' => null,
|
||||
),
|
||||
'sensitivedatareasons' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive',
|
||||
'null' => NULL_ALLOWED,
|
||||
'default' => null,
|
||||
),
|
||||
'retentionperiod' => array(
|
||||
'type' => PARAM_ALPHANUM,
|
||||
'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).',
|
||||
'default' => '',
|
||||
),
|
||||
'protected' => array(
|
||||
'type' => PARAM_INT,
|
||||
'description' => 'Data retention with higher precedent over user\'s request to be forgotten.',
|
||||
'default' => '0',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all role overrides for the purpose.
|
||||
*
|
||||
* @param purpose $purpose
|
||||
* @return array
|
||||
*/
|
||||
public static function get_overrides_for_purpose(purpose $purpose) : array {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
|
||||
$overrides = [];
|
||||
$alldata = $cache->get($purpose->get('id'));
|
||||
if (false === $alldata) {
|
||||
$tocache = [];
|
||||
foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) {
|
||||
$tocache[] = $override->to_record();
|
||||
$overrides[$override->get('roleid')] = $override;
|
||||
}
|
||||
$cache->set($purpose->get('id'), $tocache);
|
||||
} else {
|
||||
foreach ($alldata as $data) {
|
||||
$override = new self(0, $data);
|
||||
$overrides[$override->get('roleid')] = $override;
|
||||
}
|
||||
}
|
||||
|
||||
return $overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the new record to the cache.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function after_create() {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
$cache->delete($this->get('purposeid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache record.
|
||||
*
|
||||
* @param bool $result
|
||||
* @return null
|
||||
*/
|
||||
protected function after_update($result) {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
$cache->delete($this->get('purposeid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes unnecessary stuff from db.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function before_delete() {
|
||||
$cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
|
||||
$cache->delete($this->get('purposeid'));
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ 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_contexts_manager();
|
||||
$manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace());
|
||||
list($courses, $users) = $manager->process_approved_deletions();
|
||||
mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired");
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class expired_retention_period extends scheduled_task {
|
||||
* Run the task to flag context instances as expired.
|
||||
*/
|
||||
public function execute() {
|
||||
$manager = new \tool_dataprivacy\expired_contexts_manager();
|
||||
$manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace());
|
||||
list($courses, $users) = $manager->flag_expired_contexts();
|
||||
mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ $definitions = array(
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 30,
|
||||
),
|
||||
'purpose_overrides' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'simpledata' => false,
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 50,
|
||||
),
|
||||
'contextlevel' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
|
||||
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180904" COMMENT="XMLDB file for Moodle tool/dataprivacy"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -98,6 +98,9 @@
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="unexpiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Roles which have explicitly not expired yet."/>
|
||||
<FIELD NAME="expiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Explicitly expires roles"/>
|
||||
<FIELD NAME="defaultexpired" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="The default retention period has passed."/>
|
||||
<FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
@@ -146,5 +149,27 @@
|
||||
<KEY NAME="request_contextlist" TYPE="unique" FIELDS="requestid, contextlistid" COMMENT="Uniqueness constraint on request and contextlist"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="tool_dataprivacy_purposerole" COMMENT="Data purpose overrides for a specific role">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="lawfulbases" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="sensitivedatareasons" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="retentionperiod" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="purposepurposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/>
|
||||
<KEY NAME="puproseroleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="purposerole" UNIQUE="true" FIELDS="purposeid, roleid"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
@@ -184,5 +184,69 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
|
||||
upgrade_plugin_savepoint(true, 2018082100, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
if ($oldversion < 2018100401) {
|
||||
// Define table tool_dataprivacy_purposerole to be created.
|
||||
$table = new xmldb_table('tool_dataprivacy_purposerole');
|
||||
|
||||
// Adding fields to table tool_dataprivacy_purposerole.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('purposeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('retentionperiod', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('protected', XMLDB_TYPE_INTEGER, '1', null, null, null, null);
|
||||
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table tool_dataprivacy_purposerole.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('purposepurposeid', XMLDB_KEY_FOREIGN, ['purposeid'], 'tool_dataprivacy_purpose', ['id']);
|
||||
$table->add_key('puproseroleid', XMLDB_KEY_FOREIGN, ['roleid'], 'role', ['id']);
|
||||
|
||||
// Adding indexes to table tool_dataprivacy_purposerole.
|
||||
$table->add_index('purposerole', XMLDB_INDEX_UNIQUE, ['purposeid', 'roleid']);
|
||||
|
||||
// Conditionally launch create table for tool_dataprivacy_purposerole.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Update the ctxexpired table.
|
||||
$table = new xmldb_table('tool_dataprivacy_ctxexpired');
|
||||
|
||||
// Add the unexpiredroles field.
|
||||
$field = new xmldb_field('unexpiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'contextid');
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
$DB->set_field('tool_dataprivacy_ctxexpired', 'unexpiredroles', '');
|
||||
|
||||
// Add the expiredroles field.
|
||||
$field = new xmldb_field('expiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'unexpiredroles');
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
$DB->set_field('tool_dataprivacy_ctxexpired', 'expiredroles', '');
|
||||
|
||||
// Add the defaultexpired field.
|
||||
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, '1', 'expiredroles');
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Change the default for the expired field to be empty.
|
||||
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'expiredroles');
|
||||
$dbman->change_field_default($table, $field);
|
||||
|
||||
// Prevent hte field from being nullable.
|
||||
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null, 'expiredroles');
|
||||
$dbman->change_field_notnull($table, $field);
|
||||
|
||||
// Dataprivacy savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2018100401, 'tool', 'dataprivacy');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -44,14 +44,47 @@ $form = new \tool_dataprivacy\form\purpose($PAGE->url->out(false),
|
||||
$returnurl = new \moodle_url('/admin/tool/dataprivacy/purposes.php');
|
||||
if ($form->is_cancelled()) {
|
||||
redirect($returnurl);
|
||||
} else if ($data = $form->get_data()) {
|
||||
} else if ($alldata = $form->get_data()) {
|
||||
$data = $form->filter_data_for_persistent($alldata);
|
||||
|
||||
if (empty($data->id)) {
|
||||
\tool_dataprivacy\api::create_purpose($data);
|
||||
$purpose = \tool_dataprivacy\api::create_purpose($data);
|
||||
$messagesuccess = get_string('purposecreated', 'tool_dataprivacy');
|
||||
} else {
|
||||
\tool_dataprivacy\api::update_purpose($data);
|
||||
$purpose = \tool_dataprivacy\api::update_purpose($data);
|
||||
$messagesuccess = get_string('purposeupdated', 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
$currentoverrides = [];
|
||||
foreach ($purpose->get_purpose_overrides() as $override) {
|
||||
$currentoverrides[$override->get('id')] = $override;
|
||||
}
|
||||
|
||||
$overrides = $form->get_role_overrides_from_data($alldata);
|
||||
$submittedoverrides = [];
|
||||
$tosave = [];
|
||||
|
||||
foreach ($overrides as $overridedata) {
|
||||
$overridedata->purposeid = $purpose->get('id');
|
||||
$override = new \tool_dataprivacy\purpose_override($overridedata->id, $overridedata);
|
||||
|
||||
$tosave[] = $override;
|
||||
|
||||
if (!empty($overridedata->id)) {
|
||||
$submittedoverrides[$overridedata->id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($currentoverrides as $id => $override) {
|
||||
if (!isset($submittedoverrides[$id])) {
|
||||
$override->delete();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tosave as $override) {
|
||||
$override->save();
|
||||
}
|
||||
|
||||
redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ $string['approverequest'] = 'Approve request';
|
||||
$string['bulkapproverequests'] = 'Approve requests';
|
||||
$string['bulkdenyrequests'] = 'Deny requests';
|
||||
$string['cachedef_purpose'] = 'Data purposes';
|
||||
$string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data Privacy tool';
|
||||
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
|
||||
$string['cancelrequest'] = 'Cancel request';
|
||||
$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
|
||||
@@ -198,6 +199,7 @@ $string['nopurposes'] = 'There are no purposes yet';
|
||||
$string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
|
||||
$string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
|
||||
$string['notset'] = 'Not set (use the default value)';
|
||||
$string['notyetexpired'] = '{$a} (not yet expired)';
|
||||
$string['overrideinstances'] = 'Reset instances with custom values';
|
||||
$string['pluginregistry'] = 'Plugin privacy registry';
|
||||
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
|
||||
@@ -266,6 +268,7 @@ $string['retentionperiod'] = 'Retention period';
|
||||
$string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
|
||||
$string['retentionperiodnotdefined'] = 'No retention period was defined';
|
||||
$string['retentionperiodzero'] = 'No retention period';
|
||||
$string['roleoverrides'] = 'Role overrides';
|
||||
$string['selectbulkaction'] = 'Please select a bulk action.';
|
||||
$string['selectdatarequests'] = 'Please select data requests.';
|
||||
$string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.';
|
||||
@@ -291,3 +294,22 @@ $string['summary'] = 'Registry configuration summary';
|
||||
$string['user'] = 'User';
|
||||
$string['viewrequest'] = 'View the request';
|
||||
$string['visible'] = 'Expand all';
|
||||
$string['unexpiredrolewithretention'] = '{$a->retention} (Unexpired)';
|
||||
$string['expiredrolewithretention'] = '{$a->retention} (Expired)';
|
||||
$string['defaultexpired'] = 'Data for all users';
|
||||
$string['defaultexpiredexcept'] = 'Data for all users, except those who hold any of the following roles:<br>
|
||||
{$a->unexpired}';
|
||||
$string['defaultunexpiredwithexceptions'] = 'Only data for users who hold any of the following roles:<br>
|
||||
{$a->expired}
|
||||
Unless they also hold any of the following roles:<br>
|
||||
{$a->unexpired}';
|
||||
$string['defaultunexpired'] = 'Only data for users holding any of the following roles:<br>
|
||||
{$a->expired}';
|
||||
$string['tobedeleted'] = 'Data to be deleted';
|
||||
$string['addroleoverride'] = 'Add role override';
|
||||
$string['roleoverride'] = 'Role override';
|
||||
$string['role'] = 'Role';
|
||||
$string['role_help'] = 'Which role do you wish to apply this override to';
|
||||
$string['duplicaterole'] = 'Role already specified';
|
||||
$string['purposeoverview'] = 'A purpose describes the intended use and retention policy for stored data. The basis for storing and retaining that data is also described in the purpose.';
|
||||
$string['roleoverrideoverview'] = 'The default retention policy can be overridden for specific user roles, allowing you to specify a longer, or a shorter, retention policy. A user is only expired when all of their roles have expired.';
|
||||
|
||||
@@ -54,10 +54,16 @@
|
||||
}}
|
||||
|
||||
{{#navigation}}
|
||||
{{> core/action_link}}
|
||||
<div class="m-b-1">
|
||||
{{> core/action_link}}
|
||||
</div>
|
||||
{{/navigation}}
|
||||
|
||||
<div data-region="purposes" class="m-t-3 m-b-1">
|
||||
<p>
|
||||
{{#str}}purposeoverview, tool_dataprivacy{{/str}}
|
||||
</p>
|
||||
|
||||
<div data-region="purposes" class="m-b-1">
|
||||
<div class="m-y-1">
|
||||
<button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
|
||||
{{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
|
||||
@@ -72,6 +78,7 @@
|
||||
<th scope="col">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}protected, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}roleoverrides, tool_dataprivacy{{/str}}</th>
|
||||
<th scope="col">{{#str}}actions{{/str}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -115,6 +122,14 @@
|
||||
{{#str}}no{{/str}}
|
||||
{{/protected}}
|
||||
</td>
|
||||
<td>
|
||||
{{#roleoverrides}}
|
||||
{{#str}}yes{{/str}}
|
||||
{{/roleoverrides}}
|
||||
{{^roleoverrides}}
|
||||
{{#str}}no{{/str}}
|
||||
{{/roleoverrides}}
|
||||
</td>
|
||||
<td>
|
||||
{{#actions}}
|
||||
{{> core/action_menu}}
|
||||
|
||||
@@ -24,11 +24,14 @@
|
||||
|
||||
use core\invalid_persistent_exception;
|
||||
use core\task\manager;
|
||||
use tool_dataprivacy\contextlist_context;
|
||||
use tool_dataprivacy\context_instance;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\data_registry;
|
||||
use tool_dataprivacy\expired_context;
|
||||
use tool_dataprivacy\data_request;
|
||||
use tool_dataprivacy\purpose;
|
||||
use tool_dataprivacy\category;
|
||||
use tool_dataprivacy\local\helper;
|
||||
use tool_dataprivacy\task\initiate_data_request_task;
|
||||
use tool_dataprivacy\task\process_data_request_task;
|
||||
@@ -1376,6 +1379,228 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
$this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out protected purpose contexts if they are already expired.
|
||||
*/
|
||||
public function test_add_request_contexts_with_status_delete_course_expired_protected() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
$collection = new \core_privacy\local\request\contextlist_collection($user->id);
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
$contextlist->set_component('tool_dataprivacy');
|
||||
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx1)', ['ctx1' => $coursecontext->id]);
|
||||
$collection->add_contextlist($contextlist);
|
||||
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
|
||||
|
||||
$requests = contextlist_context::get_records();
|
||||
$this->assertCount(1, $requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests does filter out protected purpose contexts which are not expired.
|
||||
*/
|
||||
public function test_add_request_contexts_with_status_delete_course_unexpired_protected() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
$collection = new \core_privacy\local\request\contextlist_collection($user->id);
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
$contextlist->set_component('tool_dataprivacy');
|
||||
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx1)', ['ctx1' => $coursecontext->id]);
|
||||
$collection->add_contextlist($contextlist);
|
||||
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
|
||||
|
||||
$requests = contextlist_context::get_records();
|
||||
$this->assertCount(0, $requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out unexpired contexts if they are not protected.
|
||||
*/
|
||||
public function test_add_request_contexts_with_status_delete_course_unexpired_unprotected() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
$collection = new \core_privacy\local\request\contextlist_collection($user->id);
|
||||
$contextlist = new \core_privacy\local\request\contextlist();
|
||||
$contextlist->set_component('tool_dataprivacy');
|
||||
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx1)', ['ctx1' => $coursecontext->id]);
|
||||
$collection->add_contextlist($contextlist);
|
||||
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
|
||||
$purposes->course->set('protected', 0)->save();
|
||||
api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
|
||||
|
||||
$requests = contextlist_context::get_records();
|
||||
$this->assertCount(1, $requests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out protected purpose contexts if they are already expired.
|
||||
*/
|
||||
public function test_get_approved_contextlist_collection_for_request_delete_course_expired_protected() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
// Create the request, with its contextlist and context.
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
|
||||
$contextlist->save();
|
||||
|
||||
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
|
||||
'contextid' => $coursecontext->id,
|
||||
'status' => contextlist_context::STATUS_APPROVED,
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$clcontext->save();
|
||||
|
||||
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
|
||||
'requestid' => $request->get('id'),
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$rcl->save();
|
||||
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
$collection = api::get_approved_contextlist_collection_for_request($request);
|
||||
|
||||
$this->assertCount(1, $collection);
|
||||
|
||||
$list = $collection->get_contextlist_for_component('tool_dataprivacy');
|
||||
$this->assertCount(1, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests does filter out protected purpose contexts which are not expired.
|
||||
*/
|
||||
public function test_get_approved_contextlist_collection_for_request_delete_course_unexpired_protected() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
// Create the request, with its contextlist and context.
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
|
||||
$contextlist->save();
|
||||
|
||||
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
|
||||
'contextid' => $coursecontext->id,
|
||||
'status' => contextlist_context::STATUS_APPROVED,
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$clcontext->save();
|
||||
|
||||
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
|
||||
'requestid' => $request->get('id'),
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$rcl->save();
|
||||
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
$collection = api::get_approved_contextlist_collection_for_request($request);
|
||||
|
||||
$this->assertCount(0, $collection);
|
||||
|
||||
$list = $collection->get_contextlist_for_component('tool_dataprivacy');
|
||||
$this->assertEmpty($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that delete requests do not filter out unexpired contexts if they are not protected.
|
||||
*/
|
||||
public function test_get_approved_contextlist_collection_for_request_delete_course_unexpired_unprotected() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
|
||||
$purposes->course->set('protected', 1)->save();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
// Create the request, with its contextlist and context.
|
||||
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
|
||||
$contextlist->save();
|
||||
|
||||
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
|
||||
'contextid' => $coursecontext->id,
|
||||
'status' => contextlist_context::STATUS_APPROVED,
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$clcontext->save();
|
||||
|
||||
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
|
||||
'requestid' => $request->get('id'),
|
||||
'contextlistid' => $contextlist->get('id'),
|
||||
]);
|
||||
$rcl->save();
|
||||
|
||||
$purposes->course->set('protected', 0)->save();
|
||||
$collection = api::get_approved_contextlist_collection_for_request($request);
|
||||
|
||||
$this->assertCount(1, $collection);
|
||||
|
||||
$list = $collection->get_contextlist_for_component('tool_dataprivacy');
|
||||
$this->assertCount(1, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
|
||||
*/
|
||||
@@ -1635,4 +1860,66 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
|
||||
'list' => $approvedcollection->get_contextlist_for_component('tool_dataprivacy'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the basics with the specified retention period.
|
||||
*
|
||||
* @param string $system Retention policy for the system.
|
||||
* @param string $user Retention policy for users.
|
||||
* @param string $course Retention policy for courses.
|
||||
* @param string $activity Retention policy for activities.
|
||||
*/
|
||||
protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$purposes = (object) [
|
||||
'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM),
|
||||
'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER),
|
||||
];
|
||||
|
||||
if (null !== $course) {
|
||||
$purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
|
||||
}
|
||||
|
||||
if (null !== $activity) {
|
||||
$purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
|
||||
}
|
||||
|
||||
return $purposes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a retention period and set it for the specified context level.
|
||||
*
|
||||
* @param string $retention
|
||||
* @param int $contextlevel
|
||||
* @return purpose
|
||||
*/
|
||||
protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
|
||||
$purpose = new purpose(0, (object) [
|
||||
'name' => 'Test purpose ' . rand(1, 1000),
|
||||
'retentionperiod' => $retention,
|
||||
'lawfulbases' => 'gdpr_art_6_1_a',
|
||||
]);
|
||||
$purpose->create();
|
||||
|
||||
$cat = new category(0, (object) ['name' => 'Test category']);
|
||||
$cat->create();
|
||||
|
||||
if ($contextlevel <= CONTEXT_USER) {
|
||||
$record = (object) [
|
||||
'purposeid' => $purpose->get('id'),
|
||||
'categoryid' => $cat->get('id'),
|
||||
'contextlevel' => $contextlevel,
|
||||
];
|
||||
api::set_contextlevel($record);
|
||||
} else {
|
||||
list($purposevar, ) = data_registry::var_names_from_context(
|
||||
\context_helper::get_class_for_level(CONTEXT_COURSE)
|
||||
);
|
||||
set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
|
||||
}
|
||||
|
||||
return $purpose;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the filtered_userlist.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unit tests for the filtered_userlist.
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_filtered_userlist_testcase extends advanced_testcase {
|
||||
/**
|
||||
* Test the apply_expired_contexts_filters function with arange of options.
|
||||
*
|
||||
* @dataProvider apply_expired_contexts_filters_provider
|
||||
* @param array $initial The set of userids in the initial filterlist.
|
||||
* @param array $expired The set of userids considered as expired.
|
||||
* @param array $unexpired The set of userids considered as unexpired.
|
||||
* @param array $expected The expected values.
|
||||
*/
|
||||
public function test_apply_expired_contexts_filters(array $initial, array $expired, array $unexpired, array $expected) {
|
||||
$userlist = $this->getMockBuilder(\tool_dataprivacy\filtered_userlist::class)
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(null)
|
||||
->getMock();
|
||||
|
||||
$rc = new \ReflectionClass(\tool_dataprivacy\filtered_userlist::class);
|
||||
$rcm = $rc->getMethod('set_userids');
|
||||
$rcm->setAccessible(true);
|
||||
$rcm->invoke($userlist, $initial);
|
||||
|
||||
|
||||
$userlist->apply_expired_context_filters($expired, $unexpired);
|
||||
$filtered = $userlist->get_userids();
|
||||
|
||||
sort($expected);
|
||||
sort($filtered);
|
||||
$this->assertEquals($expected, $filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the apply_expired_contexts_filters function.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function apply_expired_contexts_filters_provider() : array {
|
||||
return [
|
||||
// Entire list should be preserved.
|
||||
'No overrides' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [],
|
||||
'unexpired' => [],
|
||||
[1, 2, 3, 4, 5],
|
||||
],
|
||||
// The list should be filtered to only keep the expired users.
|
||||
'Expired only' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [2, 3, 4],
|
||||
'unexpired' => [],
|
||||
'expected' => [2, 3, 4],
|
||||
],
|
||||
// The list should be filtered to remove any unexpired users.
|
||||
'Unexpired only' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [],
|
||||
'unexpired' => [1, 5],
|
||||
'expected' => [2, 3, 4],
|
||||
],
|
||||
// The list should be filtered to only keep expired users who are not on the unexpired list.
|
||||
'Combination of expired and unexpired' => [
|
||||
'users' => [1, 2, 3, 4, 5],
|
||||
'expired' => [1, 2, 3],
|
||||
'unexpired' => [1, 5],
|
||||
'expected' => [2, 3],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
$plugin->version = 2018092500;
|
||||
$plugin->version = 2018100403;
|
||||
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
|
||||
$plugin->component = 'tool_dataprivacy';
|
||||
|
||||
@@ -27,7 +27,9 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \tool_monitor\subscription_manager;
|
||||
use \tool_monitor\rule_manager;
|
||||
@@ -39,7 +41,10 @@ use \tool_monitor\rule_manager;
|
||||
* @copyright 2018 Adrian Greeve <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Get information about the user data stored by this plugin.
|
||||
@@ -101,6 +106,40 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_user::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextuser' => CONTEXT_USER,
|
||||
];
|
||||
|
||||
$sql = "SELECT mr.userid
|
||||
FROM {context} ctx
|
||||
JOIN {tool_monitor_rules} mr ON ctx.instanceid = mr.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "SELECT ms.userid
|
||||
FROM {context} ctx
|
||||
LEFT JOIN {tool_monitor_subscriptions} ms ON ctx.instanceid = ms.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all event monitor information for the list of contexts and this user.
|
||||
*
|
||||
@@ -142,6 +181,22 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
static::delete_user_data($contextlist->get_user()->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
$userids = $userlist->get_userids();
|
||||
$userid = reset($userids);
|
||||
|
||||
// Only delete data for user context, which should be a single user.
|
||||
if ($context->contextlevel == CONTEXT_USER && count($userids) == 1 && $userid == $context->instanceid) {
|
||||
static::delete_user_data($userid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This does the deletion of user data for the event monitor.
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \tool_monitor\privacy\provider;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy test for the event monitor
|
||||
@@ -128,6 +129,55 @@ class tool_monitor_privacy_testcase extends advanced_testcase {
|
||||
$this->assertEquals($usercontext2->id, $contextlist->get_contextids()[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the correct userlist is returned if there is any user data for this context.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'tool_monitor';
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext = \context_user::instance($user->id);
|
||||
$usercontext2 = \context_user::instance($user2->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEmpty($userlist);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEmpty($userlist);
|
||||
|
||||
$monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
|
||||
|
||||
// Create a rule with user.
|
||||
$this->setUser($user);
|
||||
$rule = $monitorgenerator->create_rule();
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
// Check that we only get back user.
|
||||
$userids = $userlist->get_userids();
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals($user->id, $userids[0]);
|
||||
|
||||
// Create a subscription with user2.
|
||||
$this->setUser($user2);
|
||||
|
||||
$record = new stdClass();
|
||||
$record->courseid = 0;
|
||||
$record->userid = $user2->id;
|
||||
$record->ruleid = $rule->id;
|
||||
|
||||
$subscription = $monitorgenerator->create_subscription($record);
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
// Check that user2 is returned for just subscribing to a rule.
|
||||
$userids = $userlist->get_userids();
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals($user2->id, $userids[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that user data is exported correctly.
|
||||
*/
|
||||
@@ -286,4 +336,80 @@ class tool_monitor_privacy_testcase extends advanced_testcase {
|
||||
$this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid);
|
||||
$this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting user data for an approved userlist in a context.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$component = 'tool_monitor';
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext = \context_user::instance($user->id);
|
||||
$usercontext2 = \context_user::instance($user2->id);
|
||||
$monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
|
||||
|
||||
$this->setUser($user);
|
||||
// Need to give user one the ability to manage rules.
|
||||
$this->assign_user_capability('tool/monitor:managerules', \context_system::instance());
|
||||
|
||||
$rulerecord = (object)['name' => 'privacy rule'];
|
||||
$rule = $monitorgenerator->create_rule($rulerecord);
|
||||
|
||||
$secondrulerecord = (object)['name' => 'privacy rule2'];
|
||||
$rule2 = $monitorgenerator->create_rule($secondrulerecord);
|
||||
|
||||
$subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id];
|
||||
$subscription = $monitorgenerator->create_subscription($subscription);
|
||||
|
||||
// Have user 2 subscribe to the second rule created by user 1.
|
||||
$subscription2 = (object)['ruleid' => $rule2->id, 'userid' => $user2->id];
|
||||
$subscription2 = $monitorgenerator->create_subscription($subscription2);
|
||||
|
||||
$this->setUser($user2);
|
||||
$thirdrulerecord = (object)['name' => 'privacy rule for second user'];
|
||||
$rule3 = $monitorgenerator->create_rule($thirdrulerecord);
|
||||
|
||||
$subscription3 = (object)['ruleid' => $rule3->id, 'userid' => $user2->id];
|
||||
$subscription3 = $monitorgenerator->create_subscription($subscription3);
|
||||
|
||||
// Get all of the monitor rules, ensure all exist.
|
||||
$dbrules = $DB->get_records('tool_monitor_rules');
|
||||
$this->assertCount(3, $dbrules);
|
||||
|
||||
// Delete for user2 in first user's context, should have no effect.
|
||||
$approveduserids = [$user2->id];
|
||||
$approvedlist = new approved_userlist($usercontext, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
$dbrules = $DB->get_records('tool_monitor_rules');
|
||||
$this->assertCount(3, $dbrules);
|
||||
|
||||
// Delete for user in usercontext.
|
||||
$approveduserids = [$user->id];
|
||||
$approvedlist = new approved_userlist($usercontext, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Only the rules for user 1 that does not have any more subscriptions should be deleted (the first rule).
|
||||
$dbrules = $DB->get_records('tool_monitor_rules');
|
||||
$this->assertCount(2, $dbrules);
|
||||
$this->assertEquals($user->id, $dbrules[$rule2->id]->userid);
|
||||
$this->assertEquals($user2->id, $dbrules[$rule3->id]->userid);
|
||||
|
||||
// There should be two subscriptions left, both for user 2.
|
||||
$dbsubs = $DB->get_records('tool_monitor_subscriptions');
|
||||
$this->assertCount(2, $dbsubs);
|
||||
$this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid);
|
||||
$this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid);
|
||||
|
||||
// Delete for user2 in context 2.
|
||||
$approveduserids = [$user2->id];
|
||||
$approvedlist = new approved_userlist($usercontext2, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// There should be no subscriptions left.
|
||||
$dbsubs = $DB->get_records('tool_monitor_subscriptions');
|
||||
$this->assertEmpty($dbsubs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,13 @@ namespace auth_mnet\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider for the mnet authentication
|
||||
@@ -39,6 +41,7 @@ use \core_privacy\local\request\writer;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -146,6 +149,33 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextuser' => CONTEXT_USER,
|
||||
'contextid' => $context->id
|
||||
];
|
||||
|
||||
$sql = "SELECT ctx.instanceid as userid
|
||||
FROM {mnet_log} ml
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = ml.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts, using the supplied exporter instance.
|
||||
*
|
||||
@@ -216,6 +246,21 @@ class provider implements
|
||||
$DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_user) {
|
||||
$DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -237,4 +282,4 @@ class provider implements
|
||||
$DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy test for the authentication mnet
|
||||
@@ -211,4 +212,119 @@ class auth_mnet_privacy_testcase extends provider_testcase {
|
||||
// There should be one (user2).
|
||||
$this->assertCount(1, $mnetlogrecords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users with a user context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'auth_mnet';
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user(['auth' => 'mnet']);
|
||||
$usercontext = context_user::instance($user->id);
|
||||
|
||||
// The list of users should not return anything yet (related data still haven't been created).
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
// Insert mnet_log record.
|
||||
$logrecord = new stdClass();
|
||||
$logrecord->hostid = '';
|
||||
$logrecord->remoteid = 65;
|
||||
$logrecord->time = time();
|
||||
$logrecord->userid = $user->id;
|
||||
$DB->insert_record('mnet_log', $logrecord);
|
||||
|
||||
// The list of users for user context should return the user.
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users for system context should not return any users.
|
||||
$systemcontext = context_system::instance();
|
||||
$userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'auth_mnet';
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user(['auth' => 'mnet']);
|
||||
$usercontext1 = context_user::instance($user1->id);
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user(['auth' => 'mnet']);
|
||||
$usercontext2 = context_user::instance($user2->id);
|
||||
|
||||
// Insert mnet_log record.
|
||||
$logrecord1 = new stdClass();
|
||||
$logrecord1->hostid = '';
|
||||
$logrecord1->remoteid = 65;
|
||||
$logrecord1->time = time();
|
||||
$logrecord1->userid = $user1->id;
|
||||
$DB->insert_record('mnet_log', $logrecord1);
|
||||
|
||||
// Insert mnet_log record.
|
||||
$logrecord2 = new stdClass();
|
||||
$logrecord2->hostid = '';
|
||||
$logrecord2->remoteid = 65;
|
||||
$logrecord2->time = time();
|
||||
$logrecord2->userid = $user2->id;
|
||||
$DB->insert_record('mnet_log', $logrecord2);
|
||||
|
||||
// The list of users for usercontext1 should return user1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
$expected = [$user1->id];
|
||||
$actual = $userlist1->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users for usercontext2 should return user2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$expected = [$user2->id];
|
||||
$actual = $userlist2->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Add userlist1 to the approved user list.
|
||||
$approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
|
||||
|
||||
// Delete user data using delete_data_for_user for usercontext1.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Re-fetch users in usercontext1 - The user list should now be empty.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
// Re-fetch users in usercontext2 - The user list should not be empty (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// User data should be only removed in the user context.
|
||||
$systemcontext = context_system::instance();
|
||||
// Add userlist2 to the approved user list in the system context.
|
||||
$approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete user1 data using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
// Re-fetch users in usercontext2 - The user list should not be empty (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ namespace auth_oauth2\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\writer;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider for auth_oauth2
|
||||
@@ -39,6 +41,7 @@ use \core_privacy\local\request\writer;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -84,6 +87,33 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextuser' => CONTEXT_USER,
|
||||
'contextid' => $context->id
|
||||
];
|
||||
|
||||
$sql = "SELECT ctx.instanceid as userid
|
||||
FROM {auth_oauth2_linked_login} ao
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = ao.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all oauth2 information for the list of contexts and this user.
|
||||
*
|
||||
@@ -126,6 +156,19 @@ class provider implements
|
||||
static::delete_user_data($context->instanceid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_user) {
|
||||
static::delete_user_data($context->instanceid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for this user only.
|
||||
*
|
||||
|
||||
@@ -28,6 +28,7 @@ use \auth_oauth2\privacy\provider;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy test for the authentication oauth2
|
||||
@@ -169,4 +170,109 @@ class auth_oauth2_privacy_testcase extends provider_testcase {
|
||||
// There should be one user.
|
||||
$this->assertCount(1, $oauth2accounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users with a user context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'auth_oauth2';
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = context_user::instance($user->id);
|
||||
|
||||
// The list of users should not return anything yet (related data still haven't been created).
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
$issuer = \core\oauth2\api::create_standard_issuer('google');
|
||||
$info = [];
|
||||
$info['username'] = 'gina';
|
||||
$info['email'] = '[email protected]';
|
||||
\auth_oauth2\api::link_login($info, $issuer, $user->id, false);
|
||||
|
||||
// The list of users for user context should return the user.
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users for system context should not return any users.
|
||||
$systemcontext = context_system::instance();
|
||||
$userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'auth_oauth2';
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$usercontext1 = context_user::instance($user1->id);
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext2 = context_user::instance($user2->id);
|
||||
|
||||
$issuer1 = \core\oauth2\api::create_standard_issuer('google');
|
||||
$info1 = [];
|
||||
$info1['username'] = 'gina1';
|
||||
$info1['email'] = '[email protected]';
|
||||
\auth_oauth2\api::link_login($info1, $issuer1, $user1->id, false);
|
||||
|
||||
$issuer2 = \core\oauth2\api::create_standard_issuer('google');
|
||||
$info2 = [];
|
||||
$info2['username'] = 'gina2';
|
||||
$info2['email'] = '[email protected]';
|
||||
\auth_oauth2\api::link_login($info2, $issuer2, $user2->id, false);
|
||||
|
||||
// The list of users for usercontext1 should return user1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
$expected = [$user1->id];
|
||||
$actual = $userlist1->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users for usercontext2 should return user2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$expected = [$user2->id];
|
||||
$actual = $userlist2->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Add userlist1 to the approved user list.
|
||||
$approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
|
||||
|
||||
// Delete user data using delete_data_for_user for usercontext1.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Re-fetch users in usercontext1 - The user list should now be empty.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
// Re-fetch users in usercontext2 - The user list should not be empty (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// User data should be only removed in the user context.
|
||||
$systemcontext = context_system::instance();
|
||||
// Add userlist2 to the approved user list in the system context.
|
||||
$approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete user1 data using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
// Re-fetch users in usercontext2 - The user list should not be empty (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for block_comments.
|
||||
@@ -40,7 +42,7 @@ use core_privacy\local\request\contextlist;
|
||||
class provider implements
|
||||
// The block_comments block stores user provided data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
// The block_comments block provides data directly to core.
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
@@ -77,6 +79,27 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'component' => 'block_comments',
|
||||
];
|
||||
|
||||
$sql = "SELECT userid as userid
|
||||
FROM {comments}
|
||||
WHERE component = :component
|
||||
AND contextid = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -104,6 +127,15 @@ class provider implements
|
||||
\core_comment\privacy\provider::delete_comments_for_all_users($context, 'block_comments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
\core_comment\privacy\provider::delete_comments_for_users($userlist, 'block_comments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use block_comments\privacy\provider;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -465,4 +466,134 @@ class block_comments_privacy_provider_testcase extends \core_privacy\tests\provi
|
||||
$DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student1->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
* @group qtesttt
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'block_comments';
|
||||
|
||||
$coursecontext1 = context_course::instance($this->course1->id);
|
||||
$coursecontext2 = context_course::instance($this->course2->id);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
|
||||
$this->setUser($this->student12);
|
||||
$this->add_comment('New comment', $coursecontext1);
|
||||
$this->add_comment('New comment', $coursecontext2);
|
||||
$this->setUser($this->student1);
|
||||
$this->add_comment('New comment', $coursecontext1);
|
||||
|
||||
// The list of users should contain user12 and user1.
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(2, $userlist1);
|
||||
$this->assertTrue(in_array($this->student1->id, $userlist1->get_userids()));
|
||||
$this->assertTrue(in_array($this->student12->id, $userlist1->get_userids()));
|
||||
|
||||
// The list of users should contain user12.
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$expected = [$this->student12->id];
|
||||
$actual = $userlist2->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$component = 'block_comments';
|
||||
|
||||
$coursecontext1 = context_course::instance($this->course1->id);
|
||||
$coursecontext2 = context_course::instance($this->course2->id);
|
||||
|
||||
$this->setUser($this->student12);
|
||||
$this->add_comment('New comment', $coursecontext1);
|
||||
$this->add_comment('New comment', $coursecontext2);
|
||||
$this->setUser($this->student1);
|
||||
$this->add_comment('New comment', $coursecontext1);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(2, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($coursecontext1, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
|
||||
// Re-fetch users in coursecontext1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in coursecontext1 should be deleted.
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// Re-fetch users in coursecontext2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in coursecontext2 should be still present.
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_user() when there are also comments from other plugins.
|
||||
*/
|
||||
public function test_delete_data_for_users_with_comments_from_other_plugins() {
|
||||
$component = 'block_comments';
|
||||
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$instance = $assigngenerator->create_instance(['course' => $this->course1]);
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$assigncontext = \context_module::instance($cm->id);
|
||||
$assign = new \assign($assigncontext, $cm, $this->course1);
|
||||
|
||||
// Add a comments block in the assignment page.
|
||||
$this->add_comments_block_in_context($assigncontext);
|
||||
|
||||
$submission = $assign->get_user_submission($this->student1->id, true);
|
||||
|
||||
$options = new stdClass();
|
||||
$options->area = 'submission_comments';
|
||||
$options->course = $assign->get_course();
|
||||
$options->context = $assigncontext;
|
||||
$options->itemid = $submission->id;
|
||||
$options->component = 'assignsubmission_comments';
|
||||
$options->showcount = true;
|
||||
$options->displaycancel = true;
|
||||
|
||||
$comment = new comment($options);
|
||||
$comment->set_post_permission(true);
|
||||
|
||||
$this->setUser($this->student1);
|
||||
$comment->add('Comment from student 1');
|
||||
|
||||
$this->add_comment('New comment', $assigncontext);
|
||||
$this->add_comment('New comment', $assigncontext);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($assigncontext, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist = new approved_userlist($assigncontext, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Re-fetch users in assigncontext.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($assigncontext, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in assigncontext should be deleted.
|
||||
$this->assertCount(0, $userlist1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ namespace block_community\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\deletion_criteria;
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\deletion_criteria;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for block_community.
|
||||
@@ -38,7 +40,10 @@ use \core_privacy\local\metadata\collection;
|
||||
* @copyright 2018 Zig Tan <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns information about how block_community stores its data.
|
||||
@@ -88,6 +93,33 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextuser' => CONTEXT_USER,
|
||||
];
|
||||
|
||||
$sql = "SELECT bc.userid as userid
|
||||
FROM {block_community} bc
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = bc.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user using the User context level.
|
||||
*
|
||||
@@ -154,6 +186,21 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
$DB->delete_records('block_community', ['userid' => $userid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_user) {
|
||||
$DB->delete_records('block_community', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,7 @@ use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \block_community\privacy\provider;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Unit tests for the block_community implementation of the privacy API.
|
||||
@@ -264,4 +265,137 @@ class block_community_privacy_testcase extends \core_privacy\tests\provider_test
|
||||
$this->assertCount(1, $communities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
|
||||
$component = 'block_community';
|
||||
|
||||
// Create a user.
|
||||
$teacher = $this->getDataGenerator()->create_user();
|
||||
$teacherctx = \context_user::instance($teacher->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($teacherctx, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
$this->setUser($teacher);
|
||||
// Add two community links for the user.
|
||||
$community = (object)[
|
||||
'userid' => $teacher->id,
|
||||
'coursename' => 'Dummy Community Course Name - 1',
|
||||
'coursedescription' => 'Dummy Community Course Description - 1',
|
||||
'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-1',
|
||||
'imageurl' => ''
|
||||
];
|
||||
$DB->insert_record('block_community', $community);
|
||||
|
||||
$community = (object)[
|
||||
'userid' => $teacher->id,
|
||||
'coursename' => 'Dummy Community Course Name - 2',
|
||||
'coursedescription' => 'Dummy Community Course Description - 2',
|
||||
'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-2',
|
||||
'imageurl' => ''
|
||||
];
|
||||
$DB->insert_record('block_community', $community);
|
||||
|
||||
// The list of users within the user context should contain user.
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$expected = [$teacher->id];
|
||||
$actual = $userlist->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users within the system context should be empty.
|
||||
$systemctx = \context_system::instance();
|
||||
$userlist2 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$component = 'block_community';
|
||||
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$userctx1 = \context_user::instance($user1->id);
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$userctx2 = \context_user::instance($user2->id);
|
||||
|
||||
$this->setUser($user1);
|
||||
// Add a community link for user1.
|
||||
$community = (object)[
|
||||
'userid' => $user1->id,
|
||||
'coursename' => 'Dummy Community Course Name - 1',
|
||||
'coursedescription' => 'Dummy Community Course Description - 1',
|
||||
'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-1',
|
||||
'imageurl' => ''
|
||||
];
|
||||
$DB->insert_record('block_community', $community);
|
||||
|
||||
// Add a community link for user1.
|
||||
$community = (object)[
|
||||
'userid' => $user1->id,
|
||||
'coursename' => 'Dummy Community Course Name - 2',
|
||||
'coursedescription' => 'Dummy Community Course Description - 2',
|
||||
'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-2',
|
||||
'imageurl' => ''
|
||||
];
|
||||
$DB->insert_record('block_community', $community);
|
||||
|
||||
$this->setUser($user2);
|
||||
// Add a community link for user2.
|
||||
$community = (object)[
|
||||
'userid' => $user2->id,
|
||||
'coursename' => 'Dummy Community Course Name - 3',
|
||||
'coursedescription' => 'Dummy Community Course Description - 3',
|
||||
'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-3',
|
||||
'imageurl' => ''
|
||||
];
|
||||
$DB->insert_record('block_community', $community);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($userctx1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($userctx2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($userctx1, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
|
||||
// Re-fetch users in userctx1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($userctx1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in userctx1 should be deleted.
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// Re-fetch users in userctx2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($userctx2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in userctx2 should be still present.
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist2 into an approved_contextlist in the system context.
|
||||
$systemcontext = \context_system::instance();
|
||||
$approvedlist2 = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// Re-fetch users in userctx2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($userctx2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in systemcontext should not be deleted.
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace block_html\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\helper;
|
||||
use \core_privacy\local\request\deletion_criteria;
|
||||
@@ -42,6 +44,9 @@ class provider implements
|
||||
// The block_html block stores user provided data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
|
||||
// The block_html block provides data directly to core.
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
@@ -87,6 +92,32 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_block::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextuser' => CONTEXT_USER,
|
||||
];
|
||||
|
||||
$sql = "SELECT bpc.instanceid AS userid
|
||||
FROM {context} c
|
||||
JOIN {block_instances} bi ON bi.id = c.instanceid AND bi.blockname = 'html'
|
||||
JOIN {context} bpc ON bpc.id = bi.parentcontextid AND bpc.contextlevel = :contextuser
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -164,6 +195,19 @@ class provider implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_block && ($blockinstance = static::get_instance_from_context($context))) {
|
||||
blocks_delete_instance($blockinstance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \block_html\privacy\provider;
|
||||
|
||||
/**
|
||||
@@ -341,4 +342,96 @@ class block_html_privacy_testcase extends \core_privacy\tests\provider_testcase
|
||||
$this->assertTrue(isset($contexts[$context->id]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users with a user context HTML block are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'block_html';
|
||||
$title = 'Block title';
|
||||
$content = 'Block content';
|
||||
$blockformat = FORMAT_PLAIN;
|
||||
|
||||
// Create a user with a user context HTML block.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user1);
|
||||
|
||||
$userblock = $this->create_user_block($title, $content, $blockformat);
|
||||
$usercontext = \context_block::instance($userblock->instance->id);
|
||||
|
||||
// Create a user with a course context HTML block.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user2);
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$courseblock = $this->create_course_block($course, $title, $content, $blockformat);
|
||||
$coursecontext = \context_block::instance($courseblock->instance->id);
|
||||
|
||||
// Ensure only the user with a user context HTML block is returned.
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
\block_html\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(1, $userlist);
|
||||
|
||||
$expected = [$user1->id];
|
||||
$actual = $userlist->get_userids();
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Ensure passing the course context returns no users.
|
||||
$userlist = new \core_privacy\local\request\userlist($coursecontext, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
$this->assertEmpty($userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'block_html';
|
||||
$title = 'Block title';
|
||||
$content = 'Block content';
|
||||
$blockformat = FORMAT_PLAIN;
|
||||
|
||||
// Create 2 user swith a user context HTML blocks.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user1);
|
||||
|
||||
$block1 = $this->create_user_block($title, $content, $blockformat);
|
||||
$context1 = \context_block::instance($block1->instance->id);
|
||||
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user2);
|
||||
$block2 = $this->create_user_block($title, $content, $blockformat);
|
||||
$context2 = \context_block::instance($block2->instance->id);
|
||||
|
||||
// Create and populate the userlists.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($context1, $component);
|
||||
\block_html\privacy\provider::get_users_in_context($userlist1);
|
||||
$userlist2 = new \core_privacy\local\request\userlist($context2, $component);
|
||||
\block_html\privacy\provider::get_users_in_context($userlist2);
|
||||
|
||||
// Ensure both members are included.
|
||||
$this->assertCount(1, $userlist1);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist = new approved_userlist($context1, 'block_html', $userlist1->get_userids());
|
||||
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Re-fetch users in the contexts - only the first one should now be empty.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($context1, $component);
|
||||
\block_html\privacy\provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($context2, $component);
|
||||
\block_html\privacy\provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace block_recent_activity\privacy;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -37,8 +39,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Shamim Rezaie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns metadata.
|
||||
@@ -71,6 +75,14 @@ class provider implements \core_privacy\local\metadata\provider,
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -87,6 +99,14 @@ class provider implements \core_privacy\local\metadata\provider,
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -28,6 +28,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy class for requesting user data.
|
||||
@@ -36,7 +38,10 @@ use \core_privacy\local\request\approved_contextlist;
|
||||
* @copyright 2018 Mihail Geshoski <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -81,6 +86,33 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextuser' => CONTEXT_USER,
|
||||
];
|
||||
|
||||
$sql = "SELECT brc.userid as userid
|
||||
FROM {block_rss_client} brc
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = brc.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -118,6 +150,19 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_user) {
|
||||
static::delete_data($context->instanceid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
use \block_rss_client\privacy\provider;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Unit tests for blocks\rss_client\classes\privacy\provider.php
|
||||
@@ -50,7 +52,7 @@ class block_rss_client_testcase extends provider_testcase {
|
||||
|
||||
$this->add_rss_feed($user);
|
||||
|
||||
$contextlist = \block_rss_client\privacy\provider::get_contexts_for_userid($user->id);
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
|
||||
$this->assertEquals($context, $contextlist->current());
|
||||
}
|
||||
@@ -80,6 +82,93 @@ class block_rss_client_testcase extends provider_testcase {
|
||||
$this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $feed1->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'block_rss_client';
|
||||
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = context_user::instance($user->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
$this->add_rss_feed($user);
|
||||
|
||||
// The list of users within the user context should contain user.
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users within the system context should be empty.
|
||||
$systemcontext = context_system::instance();
|
||||
$userlist2 = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$component = 'block_rss_client';
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$usercontext1 = context_user::instance($user1->id);
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext2 = context_user::instance($user2->id);
|
||||
|
||||
$this->add_rss_feed($user1);
|
||||
$this->add_rss_feed($user2);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
$expected = [$user1->id];
|
||||
$actual = $userlist1->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$expected = [$user2->id];
|
||||
$actual = $userlist2->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
|
||||
// Re-fetch users in usercontext1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in usercontext1 should be deleted.
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// Re-fetch users in usercontext2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in usercontext2 should be still present.
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist2 into an approved_contextlist in the system context.
|
||||
$systemcontext = context_system::instance();
|
||||
$approvedlist2 = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// Re-fetch users in usercontext2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in systemcontext should not be deleted.
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that user data is deleted using the context.
|
||||
*/
|
||||
@@ -95,7 +184,7 @@ class block_rss_client_testcase extends provider_testcase {
|
||||
$rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
|
||||
$this->assertCount(1, $rssfeeds);
|
||||
|
||||
\block_rss_client\privacy\provider::delete_data_for_all_users_in_context($context);
|
||||
provider::delete_data_for_all_users_in_context($context);
|
||||
|
||||
// Check that it has now been deleted.
|
||||
$rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
|
||||
@@ -119,7 +208,7 @@ class block_rss_client_testcase extends provider_testcase {
|
||||
|
||||
$approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'block_rss_feed',
|
||||
[$context->id]);
|
||||
\block_rss_client\privacy\provider::delete_data_for_user($approvedlist);
|
||||
provider::delete_data_for_user($approvedlist);
|
||||
|
||||
// Check that it has now been deleted.
|
||||
$rssfeeds = $DB->get_records('block_rss_client', ['userid' => $user->id]);
|
||||
|
||||
@@ -28,6 +28,8 @@ use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\context;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -37,7 +39,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Zig Tan <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -67,6 +72,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -83,6 +96,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
+24
-2
@@ -18,6 +18,7 @@
|
||||
* Privacy Subsystem implementation for cachestore_memcached.
|
||||
*
|
||||
* @package cachestore_memcached
|
||||
* @category privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -25,8 +26,10 @@
|
||||
namespace cachestore_memcached\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -36,7 +39,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -61,6 +67,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -84,4 +98,12 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
}
|
||||
|
||||
+24
-2
@@ -18,6 +18,7 @@
|
||||
* Privacy Subsystem implementation for cachestore_mongodb.
|
||||
*
|
||||
* @package cachestore_mongodb
|
||||
* @category privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -25,8 +26,10 @@
|
||||
namespace cachestore_mongodb\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -36,7 +39,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -61,6 +67,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -84,4 +98,12 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
}
|
||||
|
||||
+24
-2
@@ -18,6 +18,7 @@
|
||||
* Privacy Subsystem implementation for cachestore_redis.
|
||||
*
|
||||
* @package cachestore_redis
|
||||
* @category privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -25,8 +26,10 @@
|
||||
namespace cachestore_redis\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -36,7 +39,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -61,6 +67,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -84,4 +98,12 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
}
|
||||
|
||||
+24
-2
@@ -18,6 +18,7 @@
|
||||
* Privacy Subsystem implementation for cachestore_session.
|
||||
*
|
||||
* @package cachestore_session
|
||||
* @category privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -25,8 +26,10 @@
|
||||
namespace cachestore_session\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -36,7 +39,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -59,6 +65,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -82,4 +96,12 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy class for requesting user data.
|
||||
@@ -40,6 +42,7 @@ use core_privacy\local\request\writer;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -80,6 +83,37 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_system && !$context instanceof \context_coursecat) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextsystem' => CONTEXT_SYSTEM,
|
||||
'contextcoursecat' => CONTEXT_COURSECAT,
|
||||
];
|
||||
|
||||
$sql = "SELECT cm.userid as userid
|
||||
FROM {cohort_members} cm
|
||||
JOIN {cohort} c
|
||||
ON cm.cohortid = c.id
|
||||
JOIN {context} ctx
|
||||
ON c.contextid = ctx.id
|
||||
AND (ctx.contextlevel = :contextsystem
|
||||
OR ctx.contextlevel = :contextcoursecat)
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -150,6 +184,21 @@ class provider implements
|
||||
static::delete_data($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_system || $context instanceof \context_coursecat) {
|
||||
foreach ($userlist->get_userids() as $userid) {
|
||||
static::delete_data($context, $userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,7 @@ use core_cohort\privacy\provider;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Unit tests for cohort\classes\privacy\provider.php
|
||||
@@ -218,4 +219,124 @@ class core_cohort_testcase extends provider_testcase {
|
||||
$cohortscount = $DB->get_records('cohort');
|
||||
$this->assertCount(2, (array) $cohortscount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'core_cohort';
|
||||
|
||||
// Create system cohort and category cohort.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = \context_system::instance();
|
||||
$categorycohort = $this->getDataGenerator()->create_cohort([
|
||||
'contextid' => $coursecategoryctx->id,
|
||||
'name' => 'Category cohort 1',
|
||||
]);
|
||||
// Create user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$userctx = \context_user::instance($user->id);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecategoryctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
|
||||
$systemcohort = $this->getDataGenerator()->create_cohort([
|
||||
'contextid' => $systemctx->id,
|
||||
'name' => 'System cohort 1'
|
||||
]);
|
||||
// Create user and add to the system and category cohorts.
|
||||
cohort_add_member($categorycohort->id, $user->id);
|
||||
cohort_add_member($systemcohort->id, $user->id);
|
||||
|
||||
// The list of users within the coursecat context should contain user.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecategoryctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist1->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users within the system context should contain user.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist2->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users within the user context should be empty.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($userctx, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$component = 'core_cohort';
|
||||
|
||||
// Create system cohort and category cohort.
|
||||
$coursecategory = $this->getDataGenerator()->create_category();
|
||||
$coursecategoryctx = \context_coursecat::instance($coursecategory->id);
|
||||
$systemctx = \context_system::instance();
|
||||
$categorycohort = $this->getDataGenerator()->create_cohort([
|
||||
'contextid' => $coursecategoryctx->id,
|
||||
'name' => 'Category cohort 1',
|
||||
]);
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$userctx1 = \context_user::instance($user1->id);
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$systemcohort = $this->getDataGenerator()->create_cohort([
|
||||
'contextid' => $systemctx->id,
|
||||
'name' => 'System cohort 1'
|
||||
]);
|
||||
// Create user and add to the system and category cohorts.
|
||||
cohort_add_member($categorycohort->id, $user1->id);
|
||||
cohort_add_member($systemcohort->id, $user1->id);
|
||||
cohort_add_member($categorycohort->id, $user2->id);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecategoryctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(2, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($coursecategoryctx, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
|
||||
// Re-fetch users in coursecategoryctx.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecategoryctx, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in coursecategoryctx should be deleted.
|
||||
$this->assertCount(0, $userlist1);
|
||||
// Re-fetch users in coursecategoryctx.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in coursecontext2 should be still present.
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist2 into an approved_contextlist in the user context.
|
||||
$approvedlist3 = new approved_userlist($userctx1, $component, $userlist2->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist3);
|
||||
// Re-fetch users in coursecontext1.
|
||||
$userlist3 = new \core_privacy\local\request\userlist($systemctx, $component);
|
||||
provider::get_users_in_context($userlist3);
|
||||
// The user data in systemcontext should not be deleted.
|
||||
$this->assertCount(1, $userlist3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Privacy class for requesting user data.
|
||||
@@ -191,4 +192,64 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
$select = "userid = :userid AND component = :component $areasql $itemsql AND contextid $insql";
|
||||
$DB->delete_records_select('comments', $select, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all records for a context from a list of approved users.
|
||||
*
|
||||
* @param \core_privacy\local\request\approved_userlist $userlist Contains the list of users and
|
||||
* a context to be deleted from.
|
||||
* @param string $component Component to delete from.
|
||||
* @param string $commentarea Area to delete from.
|
||||
* @param int $itemid The item id to delete from.
|
||||
*/
|
||||
public static function delete_comments_for_users(\core_privacy\local\request\approved_userlist $userlist,
|
||||
string $component, string $commentarea = null, int $itemid = null) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'component' => $component,
|
||||
];
|
||||
$areasql = '';
|
||||
if (isset($commentarea)) {
|
||||
$params['commentarea'] = $commentarea;
|
||||
$areasql = 'AND commentarea = :commentarea';
|
||||
}
|
||||
$itemsql = '';
|
||||
if (isset($itemid)) {
|
||||
$params['itemid'] = $itemid;
|
||||
$itemsql = 'AND itemid = :itemid';
|
||||
}
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
|
||||
$params += $inparams;
|
||||
|
||||
$select = "contextid = :contextid AND component = :component {$areasql} {$itemsql} AND userid {$insql}";
|
||||
$DB->delete_records_select('comments', $select, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the list of users who have commented in the specified constraints.
|
||||
*
|
||||
* @param userlist $userlist The userlist to add the users to.
|
||||
* @param string $alias An alias prefix to use for comment selects to avoid interference with your own sql.
|
||||
* @param string $component The component to check.
|
||||
* @param string $area The comment area to check.
|
||||
* @param string $insql The SQL to use in a sub-select for the itemid query.
|
||||
* @param array $params The params required for the insql.
|
||||
*/
|
||||
public static function get_users_in_context_from_sql(
|
||||
userlist $userlist, string $alias, string $component, string $area, string $insql, $params) {
|
||||
// Comment authors.
|
||||
$sql = "SELECT {$alias}.userid
|
||||
FROM {comments} {$alias}
|
||||
WHERE {$alias}.component = :{$alias}component
|
||||
AND {$alias}.commentarea = :{$alias}commentarea
|
||||
AND {$alias}.itemid IN ({$insql})";
|
||||
|
||||
$params["{$alias}component"] = $component;
|
||||
$params["{$alias}commentarea"] = $area;
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,104 @@ class core_comment_privacy_testcase extends provider_testcase {
|
||||
$this->assertEquals('tool_dataprivacy', $data->component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests deletion of comments for a specified userlist and context.
|
||||
*/
|
||||
public function test_delete_comments_for_users() {
|
||||
global $DB;
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$course3 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
$coursecontext2 = context_course::instance($course2->id);
|
||||
$coursecontext3 = context_course::instance($course3->id);
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$comment1 = $this->get_comment_object($coursecontext1, $course1);
|
||||
$comment2 = $this->get_comment_object($coursecontext2, $course2);
|
||||
$comment3 = $this->get_comment_object($coursecontext3, $course3);
|
||||
|
||||
$this->setUser($user1);
|
||||
$comment1->add('First comment for user 1');
|
||||
$comment2->add('User 1 comment in second comment');
|
||||
|
||||
$this->setUser($user2);
|
||||
$comment2->add('User two replied in comment two');
|
||||
|
||||
$this->setUser($user3);
|
||||
$comment2->add('User 3 also writing on comment 2, but will not be deleted');
|
||||
$comment3->add('Only user 3 commenting in comment 3.');
|
||||
|
||||
// Because of the way things are set up with validation, creating an entry with the same context in a different component
|
||||
// or comment area is a huge pain. We're just going to jam entries into the table instead.
|
||||
$record = (object) [
|
||||
'contextid' => $coursecontext1->id,
|
||||
'component' => 'block_comments',
|
||||
'commentarea' => 'other_comments',
|
||||
'itemid' => 2,
|
||||
'content' => 'Comment user 1 different comment area',
|
||||
'format' => 0,
|
||||
'userid' => $user1->id,
|
||||
'timecreated' => time()
|
||||
];
|
||||
$DB->insert_record('comments', $record);
|
||||
$record = (object) [
|
||||
'contextid' => $coursecontext1->id,
|
||||
'component' => 'tool_dataprivacy',
|
||||
'commentarea' => 'page_comments',
|
||||
'itemid' => 2,
|
||||
'content' => 'Comment user 1 different component',
|
||||
'format' => 0,
|
||||
'userid' => $user1->id,
|
||||
'timecreated' => time()
|
||||
];
|
||||
$DB->insert_record('comments', $record);
|
||||
|
||||
// Delete the comments for users 1 and 2 in all 3 contexts.
|
||||
$approvedusers = [$user1->id, $user2->id];
|
||||
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($coursecontext1, 'block_comments', $approvedusers);
|
||||
\core_comment\privacy\provider::delete_comments_for_users($approveduserlist, 'block_comments', 'page_comments');
|
||||
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($coursecontext2, 'block_comments', $approvedusers);
|
||||
\core_comment\privacy\provider::delete_comments_for_users($approveduserlist, 'block_comments', 'page_comments');
|
||||
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($coursecontext3, 'block_comments', $approvedusers);
|
||||
\core_comment\privacy\provider::delete_comments_for_users($approveduserlist, 'block_comments', 'page_comments');
|
||||
|
||||
// No comments left in comments 1 as only user 1 commented there.
|
||||
$this->assertCount(0, $comment1->get_comments());
|
||||
|
||||
// Only user 3's comment left in comments 2 as user 1 and 2 were approved for deletion.
|
||||
$comment2comments = $comment2->get_comments();
|
||||
$this->assertCount(1, $comment2comments);
|
||||
$comment2comment = array_shift($comment2comments);
|
||||
$this->assertEquals($user3->id, $comment2comment->userid);
|
||||
|
||||
// Nothing changed here as user 1 and 2 did not leave a comment.
|
||||
$comment3comments = $comment3->get_comments();
|
||||
$this->assertCount(1, $comment3comments);
|
||||
$data = array_shift($comment3comments);
|
||||
$this->assertEquals($user3->id, $data->userid);
|
||||
|
||||
// Check the other comment area.
|
||||
$result = $DB->get_records('comments', ['commentarea' => 'other_comments']);
|
||||
$this->assertCount(1, $result);
|
||||
$data = array_shift($result);
|
||||
$this->assertEquals('other_comments', $data->commentarea);
|
||||
|
||||
// Check the different component, same commentarea.
|
||||
$result = $DB->get_records('comments', ['component' => 'tool_dataprivacy']);
|
||||
$this->assertCount(1, $result);
|
||||
$data = array_shift($result);
|
||||
$this->assertEquals('tool_dataprivacy', $data->component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a comment object
|
||||
*
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_enrol\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
@@ -29,6 +31,8 @@ use core_privacy\local\request\context;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem for core_enrol implementing metadata and plugin providers.
|
||||
@@ -38,6 +42,7 @@ use core_privacy\local\request\writer;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\subsystem\provider {
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -87,6 +92,36 @@ class provider implements
|
||||
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_course) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextcourse' => CONTEXT_COURSE,
|
||||
];
|
||||
|
||||
$sql = "SELECT ue.userid as userid
|
||||
FROM {user_enrolments} ue
|
||||
JOIN {enrol} e
|
||||
ON e.id = ue.enrolid
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = e.courseid
|
||||
AND ctx.contextlevel = :contextcourse
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -181,6 +216,39 @@ class provider implements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_course) {
|
||||
list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
|
||||
|
||||
$sql = "SELECT ue.id
|
||||
FROM {user_enrolments} ue
|
||||
JOIN {enrol} e
|
||||
ON e.id = ue.enrolid
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = e.courseid
|
||||
WHERE ctx.id = :contextid
|
||||
AND ue.userid {$usersql}";
|
||||
|
||||
$params = ['contextid' => $context->id] + $userparams;
|
||||
$enrolsids = $DB->get_fieldset_sql($sql, $params);
|
||||
|
||||
if (!empty($enrolsids)) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($enrolsids, SQL_PARAMS_NAMED);
|
||||
static::delete_user_data($insql, $inparams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -13,18 +13,25 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for enrol_cohort.
|
||||
*
|
||||
* @package enrol_cohort
|
||||
* @category privacy
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace enrol_cohort\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider for enrol_cohort.
|
||||
@@ -33,8 +40,15 @@ use \core_privacy\local\request\approved_contextlist;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
// This plugin stores user data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This plugin contains user's enrolments.
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
*
|
||||
@@ -46,6 +60,7 @@ class provider implements
|
||||
$collection->add_subsystem_link('core_group', [], 'privacy:metadata:core_group');
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
*
|
||||
@@ -53,24 +68,24 @@ class provider implements
|
||||
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
$contextlist = new contextlist();
|
||||
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextlevel
|
||||
WHERE gm.userid = :userid
|
||||
AND gm.component = 'enrol_cohort'";
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
'userid' => $userid
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
return \core_group\privacy\provider::get_contexts_for_group_member($userid, 'enrol_cohort');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_course) {
|
||||
return;
|
||||
}
|
||||
|
||||
\core_group\privacy\provider::get_group_members_in_context($userlist, 'enrol_cohort');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -105,6 +120,7 @@ class provider implements
|
||||
\core_group\privacy\provider::delete_groups_for_all_users($context, 'enrol_cohort');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -116,4 +132,14 @@ class provider implements
|
||||
}
|
||||
\core_group\privacy\provider::delete_groups_for_user($contextlist, 'enrol_cohort');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
\core_group\privacy\provider::delete_groups_for_users($userlist, 'enrol_cohort');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Base class for unit tests for enrol_cohort.
|
||||
*
|
||||
@@ -21,10 +22,13 @@
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \enrol_cohort\privacy\provider;
|
||||
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use enrol_cohort\privacy\provider;
|
||||
|
||||
/**
|
||||
* Unit tests for the enrol_cohort implementation of the privacy API.
|
||||
*
|
||||
@@ -32,6 +36,7 @@ use \enrol_cohort\privacy\provider;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class enrol_cohort_privacy_testcase extends \core_privacy\tests\provider_testcase {
|
||||
|
||||
/**
|
||||
* Test getting the context for the user ID related to this plugin.
|
||||
*/
|
||||
@@ -123,6 +128,7 @@ class enrol_cohort_privacy_testcase extends \core_privacy\tests\provider_testcas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_all_users_in_context().
|
||||
*/
|
||||
@@ -170,6 +176,7 @@ class enrol_cohort_privacy_testcase extends \core_privacy\tests\provider_testcas
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_user().
|
||||
*/
|
||||
@@ -249,4 +256,138 @@ class enrol_cohort_privacy_testcase extends \core_privacy\tests\provider_testcas
|
||||
WHERE g.courseid = ?", [$course2->id])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$trace = new null_progress_trace();
|
||||
|
||||
$cohortplugin = enrol_get_plugin('cohort');
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$cat1 = $this->getDataGenerator()->create_category();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course(array('category' => $cat1->id));
|
||||
$course2 = $this->getDataGenerator()->create_course(array('category' => $cat1->id));
|
||||
|
||||
$group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group2 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
|
||||
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
|
||||
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort(
|
||||
array('contextid' => context_coursecat::instance($cat1->id)->id));
|
||||
$cohortplugin->add_instance($course1, array(
|
||||
'customint1' => $cohort1->id,
|
||||
'roleid' => $studentrole->id,
|
||||
'customint2' => $group1->id)
|
||||
);
|
||||
$cohortplugin->add_instance($course2, array(
|
||||
'customint1' => $cohort1->id,
|
||||
'roleid' => $studentrole->id,
|
||||
'customint2' => $group2->id)
|
||||
);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course1->id);
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id));
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user3->id));
|
||||
|
||||
cohort_add_member($cohort1->id, $user1->id);
|
||||
enrol_cohort_sync($trace, $course1->id);
|
||||
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course2->id])
|
||||
);
|
||||
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'enrol_cohort',
|
||||
[$user1->id, $user2->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
// Check we have 2 users in groups because we have deleted user1.
|
||||
// User2's membership is manual and is not as the result of a cohort enrolment.
|
||||
$this->assertEquals(
|
||||
2,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
|
||||
// Check that course2 is not touched.
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course2->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$trace = new null_progress_trace();
|
||||
|
||||
$cohortplugin = enrol_get_plugin('cohort');
|
||||
|
||||
$cat1 = $this->getDataGenerator()->create_category();
|
||||
$course1 = $this->getDataGenerator()->create_course(array('category' => $cat1->id));
|
||||
$group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
|
||||
$cohort1 = $this->getDataGenerator()->create_cohort(
|
||||
array('contextid' => context_coursecat::instance($cat1->id)->id));
|
||||
$cohortplugin->add_instance($course1, array(
|
||||
'customint1' => $cohort1->id,
|
||||
'roleid' => $studentrole->id,
|
||||
'customint2' => $group1->id)
|
||||
);
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
|
||||
cohort_add_member($cohort1->id, $user1->id);
|
||||
enrol_cohort_sync($trace, $course1->id);
|
||||
|
||||
// Check if user1 is enrolled into course1 in group 1.
|
||||
$this->assertEquals(1, $DB->count_records('role_assignments', array()));
|
||||
$this->assertTrue($DB->record_exists('groups_members', array(
|
||||
'groupid' => $group1->id,
|
||||
'userid' => $user1->id,
|
||||
'component' => 'enrol_cohort')
|
||||
));
|
||||
|
||||
$context = \context_course::instance($course1->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, 'enrol_cohort');
|
||||
\enrol_cohort\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEquals([$user1->id], $userlist->get_userids());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,22 +13,29 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for enrol_flatfile.
|
||||
*
|
||||
* @package enrol_flatfile
|
||||
* @category privacy
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace enrol_flatfile\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\context;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\transform;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Privacy Subsystem for enrol_flatfile implementing null_provider.
|
||||
*
|
||||
@@ -37,7 +44,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -75,6 +83,23 @@ class provider implements
|
||||
return $contextlist->add_from_sql($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context->contextlevel != CONTEXT_COURSE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "SELECT userid FROM {enrol_flatfile} WHERE courseid = ?";
|
||||
$params = [$context->instanceid];
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -146,7 +171,7 @@ class provider implements
|
||||
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
// Only delete data from contexts which are at the COURSE_MODULE contextlevel.
|
||||
// Only delete data from contexts which are at the CONTEXT_COURSE contextlevel.
|
||||
$contexts = self::validate_contextlist_contexts($contextlist);
|
||||
if (empty($contexts)) {
|
||||
return;
|
||||
@@ -165,6 +190,28 @@ class provider implements
|
||||
$DB->delete_records_select('enrol_flatfile', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context->contextlevel != CONTEXT_COURSE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$params = array_merge(['courseid' => $context->instanceid], $inparams);
|
||||
$sql = "courseid = :courseid AND userid $insql";
|
||||
$DB->delete_records_select('enrol_flatfile', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple sanity check on the contextlist contexts, making sure they're of CONTEXT_COURSE contextlevel.
|
||||
*
|
||||
|
||||
@@ -45,6 +45,12 @@ class enrol_flatfile_privacy_testcase extends provider_testcase {
|
||||
/** @var \stdClass $user2 a test user.*/
|
||||
protected $user2;
|
||||
|
||||
/** @var \stdClass $user3 a test user.*/
|
||||
protected $user3;
|
||||
|
||||
/** @var \stdClass $user4 a test user.*/
|
||||
protected $user4;
|
||||
|
||||
/** @var \context $coursecontext1 a course context.*/
|
||||
protected $coursecontext1;
|
||||
|
||||
@@ -80,7 +86,7 @@ class enrol_flatfile_privacy_testcase extends provider_testcase {
|
||||
// Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
|
||||
$this->create_future_enrolments();
|
||||
|
||||
$this->assertEquals(3, $DB->count_records('enrol_flatfile'));
|
||||
$this->assertEquals(5, $DB->count_records('enrol_flatfile'));
|
||||
|
||||
// We expect to see 2 entries for user1, in course1 and course3.
|
||||
$contextlist = provider::get_contexts_for_userid($this->user1->id);
|
||||
@@ -166,10 +172,10 @@ class enrol_flatfile_privacy_testcase extends provider_testcase {
|
||||
// Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
|
||||
$this->create_future_enrolments();
|
||||
|
||||
// Verify we have 1 future enrolments for course 1.
|
||||
$this->assertEquals(1, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
|
||||
// Verify we have 3 future enrolments for course 1.
|
||||
$this->assertEquals(3, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
|
||||
|
||||
// Now, run delete by context and confirm that record is removed.
|
||||
// Now, run delete by context and confirm that all records are removed.
|
||||
provider::delete_data_for_all_users_in_context($this->coursecontext1);
|
||||
$this->assertEquals(0, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
|
||||
}
|
||||
@@ -199,6 +205,68 @@ class enrol_flatfile_privacy_testcase extends provider_testcase {
|
||||
$this->assertEquals(0, $DB->count_records('enrol_flatfile', ['userid' => $this->user1->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
// Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
|
||||
$this->create_future_enrolments();
|
||||
|
||||
$this->assertEquals(5, $DB->count_records('enrol_flatfile'));
|
||||
|
||||
// We expect to see 3 entries for course1, and that's user1, user3 and user4.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->coursecontext1, 'enrol_flatfile');
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEquals(
|
||||
[$this->user1->id, $this->user3->id, $this->user4->id],
|
||||
$userlist->get_userids(),
|
||||
'', 0.0, 10, true
|
||||
);
|
||||
|
||||
// And 1 for course2 which is for user2.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->coursecontext2, 'enrol_flatfile');
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEquals([$this->user2->id], $userlist->get_userids());
|
||||
|
||||
// And 1 for course3 which is for user1 again.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->coursecontext3, 'enrol_flatfile');
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEquals([$this->user1->id], $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
// Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
|
||||
$this->create_future_enrolments();
|
||||
|
||||
// Verify we have 3 future enrolment for user 1, user 3 and user 4.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->coursecontext1, 'enrol_flatfile');
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEquals(
|
||||
[$this->user1->id, $this->user3->id, $this->user4->id],
|
||||
$userlist->get_userids(),
|
||||
'', 0.0, 10, true
|
||||
);
|
||||
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($this->coursecontext1, 'enrol_flatfile',
|
||||
[$this->user1->id, $this->user3->id]);
|
||||
|
||||
// Now, run delete for user and confirm that the record is removed.
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
$userlist = new \core_privacy\local\request\userlist($this->coursecontext1, 'enrol_flatfile');
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEquals([$this->user4->id], $userlist->get_userids());
|
||||
$this->assertEquals(
|
||||
[$this->user4->id],
|
||||
$DB->get_fieldset_select('enrol_flatfile', 'userid', 'courseid = ?', [$this->coursecontext1->instanceid])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to sync a file and create the enrol_flatfile DB entries, for use with the get, export and delete tests.
|
||||
*/
|
||||
@@ -206,6 +274,8 @@ class enrol_flatfile_privacy_testcase extends provider_testcase {
|
||||
global $CFG;
|
||||
$this->user1 = $this->getDataGenerator()->create_user(['idnumber' => 'u1']);
|
||||
$this->user2 = $this->getDataGenerator()->create_user(['idnumber' => 'u2']);
|
||||
$this->user3 = $this->getDataGenerator()->create_user(['idnumber' => 'u3']);
|
||||
$this->user4 = $this->getDataGenerator()->create_user(['idnumber' => 'u4']);
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course(['idnumber' => 'c1']);
|
||||
$course2 = $this->getDataGenerator()->create_course(['idnumber' => 'c2']);
|
||||
@@ -221,6 +291,8 @@ class enrol_flatfile_privacy_testcase extends provider_testcase {
|
||||
$file = "$CFG->dataroot/enrol.txt";
|
||||
$data = "add,student,u1,c1,$future,0
|
||||
add,student,u2,c2,$future,0
|
||||
add,student,u3,c1,$future,0
|
||||
add,student,u4,c1,$future,0
|
||||
add,student,u1,c3,$future,$farfuture";
|
||||
file_put_contents($file, $data);
|
||||
|
||||
|
||||
@@ -13,18 +13,25 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for enrol_meta.
|
||||
*
|
||||
* @package enrol_meta
|
||||
* @category privacy
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace enrol_meta\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider for enrol_meta.
|
||||
@@ -33,8 +40,15 @@ use \core_privacy\local\request\approved_contextlist;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
// This plugin stores user data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This plugin contains user's enrolments.
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
*
|
||||
@@ -46,6 +60,7 @@ class provider implements
|
||||
$collection->add_subsystem_link('core_group', [], 'privacy:metadata:core_group');
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
*
|
||||
@@ -53,24 +68,24 @@ class provider implements
|
||||
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
$contextlist = new contextlist();
|
||||
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextlevel
|
||||
WHERE gm.userid = :userid
|
||||
AND gm.component = 'enrol_meta'";
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
'userid' => $userid
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
return \core_group\privacy\provider::get_contexts_for_group_member($userid, 'enrol_meta');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_course) {
|
||||
return;
|
||||
}
|
||||
|
||||
\core_group\privacy\provider::get_group_members_in_context($userlist, 'enrol_meta');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -105,6 +120,7 @@ class provider implements
|
||||
\core_group\privacy\provider::delete_groups_for_all_users($context, 'enrol_meta');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -116,4 +132,13 @@ class provider implements
|
||||
}
|
||||
\core_group\privacy\provider::delete_groups_for_user($contextlist, 'enrol_meta');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
\core_group\privacy\provider::delete_groups_for_users($userlist, 'enrol_meta');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Base class for unit tests for enrol_meta.
|
||||
*
|
||||
@@ -21,10 +22,13 @@
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \enrol_meta\privacy\provider;
|
||||
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use enrol_meta\privacy\provider;
|
||||
|
||||
/**
|
||||
* Unit tests for the enrol_meta implementation of the privacy API.
|
||||
*
|
||||
@@ -32,6 +36,7 @@ use \enrol_meta\privacy\provider;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class enrol_meta_privacy_testcase extends \core_privacy\tests\provider_testcase {
|
||||
|
||||
/**
|
||||
* Enable enrol_meta plugin.
|
||||
*/
|
||||
@@ -41,6 +46,7 @@ class enrol_meta_privacy_testcase extends \core_privacy\tests\provider_testcase
|
||||
$enabled = array_keys($enabled);
|
||||
set_config('enrol_plugins_enabled', implode(',', $enabled));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting the context for the user ID related to this plugin.
|
||||
*/
|
||||
@@ -111,6 +117,7 @@ class enrol_meta_privacy_testcase extends \core_privacy\tests\provider_testcase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_all_users_in_context().
|
||||
*/
|
||||
@@ -150,6 +157,7 @@ class enrol_meta_privacy_testcase extends \core_privacy\tests\provider_testcase
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_user().
|
||||
*/
|
||||
@@ -195,4 +203,100 @@ class enrol_meta_privacy_testcase extends \core_privacy\tests\provider_testcase
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$metaplugin = enrol_get_plugin('meta');
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
|
||||
$this->enable_plugin();
|
||||
$metaplugin->add_instance($course1, array('customint1' => $course2->id, 'customint2' => $group1->id));
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'student');
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course2->id, 'student');
|
||||
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'enrol_meta',
|
||||
[$user1->id, $user2->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
// Check we have 1 user in groups because we have deleted user1 and user2.
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$metaplugin = enrol_get_plugin('meta');
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
|
||||
$this->enable_plugin();
|
||||
$metaplugin->add_instance($course1, array('customint1' => $course2->id, 'customint2' => $group1->id));
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'student');
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
|
||||
|
||||
// Check if user1 is enrolled into course1 in group 1.
|
||||
$this->assertTrue(groups_is_member($group1->id, $user1->id));
|
||||
$this->assertTrue($DB->record_exists('groups_members',
|
||||
array(
|
||||
'groupid' => $group1->id,
|
||||
'userid' => $user1->id,
|
||||
'component' => 'enrol_meta'
|
||||
)
|
||||
));
|
||||
|
||||
$context = \context_course::instance($course1->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, 'enrol_meta');
|
||||
\enrol_meta\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEquals(
|
||||
[$user1->id, $user2->id],
|
||||
$userlist->get_userids(),
|
||||
'', 0.0, 10, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
* @copyright 2018 Carlos Escobedo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_enrol\privacy\provider;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use \core_privacy\local\request\transform;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy test for the core_enrol.
|
||||
*
|
||||
@@ -172,4 +176,96 @@ class core_enrol_privacy_testcase extends provider_testcase {
|
||||
$userenrolments = $DB->get_records('user_enrolments', array());
|
||||
$this->assertCount(3, $userenrolments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users within a course context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'core_enrol';
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = context_user::instance($user->id);
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecontext, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// Enrol user into course.
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course->id, null, 'manual');
|
||||
|
||||
// The list of users within the course context should contain user.
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist1->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users within the user context should be empty.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'core_enrol';
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
$coursecontext2 = context_course::instance($course2->id);
|
||||
$systemcontext = context_system::instance();
|
||||
|
||||
// Enrol user1 into course1.
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'manual');
|
||||
// Enrol user2 into course1.
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'manual');
|
||||
// Enrol user3 into course2.
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course2->id, null, 'manual');
|
||||
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(2, $userlist1);
|
||||
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist1 into an approved_contextlist.
|
||||
$approvedlist1 = new approved_userlist($coursecontext1, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist1);
|
||||
// Re-fetch users in coursecontext1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
// The user data in coursecontext1 should be deleted.
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// Re-fetch users in coursecontext2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in coursecontext2 should be still present.
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// Convert $userlist2 into an approved_contextlist in the system context.
|
||||
$approvedlist2 = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist2);
|
||||
// Re-fetch users in coursecontext1.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
// The user data in systemcontext should not be deleted.
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use context;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Data provider class.
|
||||
@@ -39,8 +41,9 @@ use core_privacy\local\request\approved_contextlist;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\subsystem\provider {
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\subsystem\provider {
|
||||
|
||||
/**
|
||||
* Returns metadata.
|
||||
@@ -69,6 +72,14 @@ class provider implements
|
||||
return new \core_privacy\local\request\contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -85,6 +96,14 @@ class provider implements
|
||||
public static function delete_data_for_all_users_in_context(context $context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -29,8 +29,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for core_group.
|
||||
@@ -46,7 +48,10 @@ class provider implements
|
||||
\core_privacy\local\request\subsystem\provider,
|
||||
|
||||
// The group subsystem can provide information to other plugins.
|
||||
\core_privacy\local\request\subsystem\plugin_provider {
|
||||
\core_privacy\local\request\subsystem\plugin_provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -191,30 +196,130 @@ class provider implements
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
* Add the list of users who are members of some groups in the specified constraints.
|
||||
*
|
||||
* @param int $userid The user to search.
|
||||
* @return contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
* @param userlist $userlist The userlist to add the users to.
|
||||
* @param string $component The component to check.
|
||||
* @param int $itemid Optional itemid associated with component.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
public static function get_group_members_in_context(userlist $userlist, string $component, int $itemid = 0) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_course) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Group members in the given context.
|
||||
$sql = "SELECT gm.userid
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = :courseid AND gm.component = :component";
|
||||
$params = [
|
||||
'courseid' => $context->instanceid,
|
||||
'component' => $component
|
||||
];
|
||||
|
||||
if ($itemid) {
|
||||
$sql .= ' AND gm.itemid = :itemid';
|
||||
$params['itemid'] = $itemid;
|
||||
}
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all records for multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
* @param string $component Component to delete from. Empty string means no component (manual memberships).
|
||||
* @param int $itemid Optional itemid associated with component.
|
||||
*/
|
||||
public static function delete_groups_for_users(approved_userlist $userlist, string $component, int $itemid = 0) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
|
||||
$groupselect = "SELECT g.id
|
||||
FROM {groups} g
|
||||
JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
|
||||
WHERE ctx.id = :contextid";
|
||||
$groupparams = ['contextid' => $context->id, 'contextcourse' => CONTEXT_COURSE];
|
||||
|
||||
$select = "component = :component AND userid {$usersql} AND groupid IN ({$groupselect})";
|
||||
$params = ['component' => $component] + $groupparams + $userparams;
|
||||
|
||||
if ($itemid) {
|
||||
$select .= ' AND itemid = :itemid';
|
||||
$params['itemid'] = $itemid;
|
||||
}
|
||||
|
||||
$DB->delete_records_select('groups_members', $select, $params);
|
||||
|
||||
// Invalidate the group and grouping cache for the user.
|
||||
\cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), $userids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain group membership for the specified user.
|
||||
*
|
||||
* @param int $userid The user to search.
|
||||
* @param string $component The component to check.
|
||||
* @param int $itemid Optional itemid associated with component.
|
||||
* @return contextlist The contextlist containing the list of contexts.
|
||||
*/
|
||||
public static function get_contexts_for_group_member(int $userid, string $component, int $itemid = 0) {
|
||||
$contextlist = new contextlist();
|
||||
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
|
||||
WHERE gm.userid = :userid";
|
||||
WHERE gm.userid = :userid AND gm.component = :component";
|
||||
|
||||
$params = [
|
||||
'contextcourse' => CONTEXT_COURSE,
|
||||
'userid' => $userid
|
||||
'userid' => $userid,
|
||||
'component' => $component
|
||||
];
|
||||
|
||||
if ($itemid) {
|
||||
$sql .= ' AND gm.itemid = :itemid';
|
||||
$params['itemid'] = $itemid;
|
||||
}
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param int $userid The user to search.
|
||||
* @return contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : contextlist {
|
||||
return static::get_contexts_for_group_member($userid, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_course) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::get_group_members_in_context($userlist, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -245,4 +350,14 @@ class provider implements
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
static::delete_groups_for_user($contextlist, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
static::delete_groups_for_users($userlist, '');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -581,6 +581,43 @@ class core_group_privacy_provider_testcase extends provider_testcase {
|
||||
'', 0.0, 10, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_contexts_for_userid() when there are group memberships from other components.
|
||||
*/
|
||||
public function test_get_contexts_for_userid_component() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group2 = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course2->id);
|
||||
|
||||
$this->getDataGenerator()->create_group_member(
|
||||
array(
|
||||
'userid' => $user->id,
|
||||
'groupid' => $group1->id
|
||||
));
|
||||
$this->getDataGenerator()->create_group_member(
|
||||
array(
|
||||
'userid' => $user->id,
|
||||
'groupid' => $group2->id,
|
||||
'component' => 'enrol_meta'
|
||||
));
|
||||
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
|
||||
// User is member of some groups in course1 and course2,
|
||||
// but only the membership in course1 is directly managed by core_group.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$this->assertEquals([$coursecontext1->id], $contextlist->get_contextids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::export_user_data().
|
||||
*/
|
||||
@@ -789,4 +826,120 @@ class core_group_privacy_provider_testcase extends provider_testcase {
|
||||
WHERE gm.userid = ?", [$user1->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group1c = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
$group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
$group2c = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course2->id);
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course2->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course2->id);
|
||||
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id));
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group1b->id, 'userid' => $user2->id));
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group1c->id, 'userid' => $user3->id));
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id));
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group2b->id, 'userid' => $user2->id));
|
||||
$this->getDataGenerator()->create_group_member(array('groupid' => $group2c->id, 'userid' => $user3->id));
|
||||
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course2->id])
|
||||
);
|
||||
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'core_group',
|
||||
[$user1->id, $user2->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
$this->assertEquals(
|
||||
[$user3->id],
|
||||
$DB->get_fieldset_sql("SELECT gm.userid
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course1->id])
|
||||
);
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$DB->count_records_sql("SELECT COUNT(gm.id)
|
||||
FROM {groups_members} gm
|
||||
JOIN {groups} g ON gm.groupid = g.id
|
||||
WHERE g.courseid = ?", [$course2->id])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
|
||||
$group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
$group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id));
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course2->id);
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course2->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course1->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course2->id);
|
||||
|
||||
$this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1a->id));
|
||||
$this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group2a->id));
|
||||
$this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group1b->id));
|
||||
$this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $group2b->id));
|
||||
$this->getDataGenerator()->create_group_member(array('userid' => $user3->id, 'groupid' => $group2a->id));
|
||||
|
||||
$coursecontext1 = context_course::instance($course1->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($coursecontext1, 'core_group');
|
||||
\core_group\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// Only user1 and user2. User3 is not member of any group in course1.
|
||||
$this->assertCount(2, $userlist);
|
||||
$this->assertEquals(
|
||||
[$user1->id, $user2->id],
|
||||
$userlist->get_userids(),
|
||||
'', 0.0, 10, true);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -33,9 +33,12 @@ $string['trace:exportingapproved'] = 'Performing primary export for {$a->total}
|
||||
$string['trace:exportingrelated'] = 'Performing related export for {$a->total} components ({$a->datetime})';
|
||||
$string['trace:finalisingexport'] = 'Finalising export';
|
||||
$string['trace:processingcomponent'] = 'Processing {$a->component} ({$a->progress}/{$a->total}) ({$a->datetime})';
|
||||
$string['trace:fetchcomponents'] = 'Fetching {$a->total} components ({$a->datetime})';
|
||||
$string['trace:deletingapproved'] = 'Performing removal of approved {$a->total} contexts ({$a->datetime})';
|
||||
$string['trace:preprocessingcomponent'] = 'Pre-processing {$a->component} ({$a->progress}/{$a->total}) ({$a->datetime})';
|
||||
$string['trace:fetchcomponents'] = 'Fetching data from {$a->total} components ({$a->datetime})';
|
||||
$string['trace:deletingapproved'] = 'Performing removal of {$a->total} approved contexts ({$a->datetime})';
|
||||
$string['trace:deletingapprovedusers'] = 'Performing removal of users in {$a->total} approved component for context {$a->contextid} ({$a->datetime})';
|
||||
$string['trace:deletingcontext'] = 'Performing removal of context from {$a->total} components ({$a->datetime})';
|
||||
$string['navigation'] = 'Navigation';
|
||||
$string['trace:deletinguser'] = 'Performing removal of user from {$a->total} components ({$a->datetime})';
|
||||
$string['privacy:subsystem:empty'] = 'This subsystem does not store any data.';
|
||||
$string['viewdata'] = 'Click on a link in the navigation to view data.';
|
||||
|
||||
@@ -30,6 +30,8 @@ use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy class for requesting user data.
|
||||
@@ -38,7 +40,10 @@ use \core_privacy\local\request\transform;
|
||||
* @copyright 2018 Adrian Greeve <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -88,6 +93,35 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextuser' => CONTEXT_USER,
|
||||
];
|
||||
|
||||
$sql = "SELECT ud.userid as userid
|
||||
FROM {message_airnotifier_devices} mad
|
||||
JOIN {user_devices} ud
|
||||
ON ud.id = mad.userdeviceid
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = ud.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -129,6 +163,19 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
static::delete_data($context->instanceid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_user) {
|
||||
static::delete_data($context->instanceid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
use \message_airnotifier\privacy\provider;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Unit tests for message\output\airnotifier\classes\privacy\provider.php
|
||||
*
|
||||
@@ -160,4 +163,89 @@ class message_airnotifier_testcase extends provider_testcase {
|
||||
$devices = $DB->get_records('message_airnotifier_devices');
|
||||
$this->assertCount(0, $devices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users with a user context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'message_airnotifier';
|
||||
|
||||
// Create user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = context_user::instance($user->id);
|
||||
|
||||
// The lists of users for the user context should be empty.
|
||||
// Related user data have not been created yet.
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
$this->add_device($user, 'apuJih874kj');
|
||||
$this->add_device($user, 'bdu09Ikjjsu');
|
||||
|
||||
// The list of users for userlist should return one user (user).
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users should only return users in the user context.
|
||||
$systemcontext = context_system::instance();
|
||||
$userlist1 = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$component = 'message_airnotifier';
|
||||
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$usercontext1 = context_user::instance($user1->id);
|
||||
// Create user2.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext2 = context_user::instance($user2->id);
|
||||
|
||||
$this->add_device($user1, 'apuJih874kj');
|
||||
$this->add_device($user1, 'cpuJih874kp');
|
||||
$this->add_device($user2, 'bdu09Ikjjsu');
|
||||
|
||||
// The list of users for usercontext1 should return one user (user1).
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
|
||||
// The list of users for usercontext2 should return one user (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
$approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Re-fetch users in usercontext1 - the user data should now be empty.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
|
||||
// The list of users for usercontext2 should still return one user (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// User data should only be removed in the user context.
|
||||
$systemcontext = context_system::instance();
|
||||
$approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
// Re-fetch users in usercontext2 - the user data should still be present.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy class for requesting user data.
|
||||
@@ -37,7 +39,10 @@ use \core_privacy\local\request\approved_contextlist;
|
||||
* @copyright 2018 Mihail Geshoski <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
* Returns meta data about this system.
|
||||
@@ -72,6 +77,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -88,6 +101,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -28,6 +28,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
use \core_privacy\local\metadata\collection;
|
||||
use \core_privacy\local\request\contextlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy class for requesting user data.
|
||||
@@ -38,6 +40,7 @@ use \core_privacy\local\request\approved_contextlist;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -66,6 +69,14 @@ class provider implements
|
||||
return new contextlist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -82,6 +93,14 @@ class provider implements
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,8 @@ use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\userlist;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -45,7 +47,8 @@ require_once($CFG->dirroot . '/mod/assignment/lib.php');
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\user_preference_provider {
|
||||
\core_privacy\local\request\user_preference_provider,
|
||||
\core_privacy\local\request\core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Return the fields which contain personal data.
|
||||
@@ -112,6 +115,43 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
if ($context->contextlevel != CONTEXT_MODULE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'modulename' => 'assignment',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'contextid' => $context->id
|
||||
];
|
||||
$sql = "SELECT s.userid
|
||||
FROM {assignment_submissions} s
|
||||
JOIN {assignment} a ON s.assignment = a.id
|
||||
JOIN {modules} m ON m.name = :modulename
|
||||
JOIN {course_modules} cm ON a.id = cm.instance AND cm.module = m.id
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
WHERE ctx.id = :contextid
|
||||
";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "SELECT s.teacher
|
||||
FROM {assignment_submissions} s
|
||||
JOIN {assignment} a ON s.assignment = a.id
|
||||
JOIN {modules} m ON m.name = :modulename
|
||||
JOIN {course_modules} cm ON a.id = cm.instance AND cm.module = m.id
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
WHERE ctx.id = :contextid
|
||||
";
|
||||
$userlist->add_from_sql('teacher', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export personal data for the given approved_contextlist.
|
||||
* User and context information is contained within the contextlist.
|
||||
@@ -255,6 +295,43 @@ class provider implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
// If the context isn't for a module then return early.
|
||||
if ($context->contextlevel != CONTEXT_MODULE) {
|
||||
return;
|
||||
}
|
||||
// Fetch the assignment.
|
||||
$assignment = self::get_assignment_by_context($context);
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
list($inorequalsql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$params['assignmentid'] = $assignment->id;
|
||||
|
||||
// Get submission ids.
|
||||
$sql = "
|
||||
SELECT s.id
|
||||
FROM {assignment_submissions} s
|
||||
JOIN {assignment} a ON s.assignment = a.id
|
||||
WHERE a.id = :assignmentid
|
||||
AND s.userid $inorequalsql
|
||||
";
|
||||
|
||||
$submissionids = $DB->get_records_sql($sql, $params);
|
||||
list($submissionidsql, $submissionparams) = $DB->get_in_or_equal(array_keys($submissionids), SQL_PARAMS_NAMED);
|
||||
$fs = get_file_storage();
|
||||
$fs->delete_area_files_select($context->id, 'mod_assignment', 'submission', $submissionidsql, $submissionparams);
|
||||
// Delete related tables.
|
||||
$DB->delete_records_list('assignment_submissions', 'id', array_keys($submissionids));
|
||||
}
|
||||
|
||||
// Start of helper functions.
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,76 @@ class mod_assignment_privacy_testcase extends advanced_testcase {
|
||||
$this->assertEquals(2, count($contextids->get_contextids()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the correct userids are returned for a specific context.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
|
||||
$student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
|
||||
// Student 3 should not turn up in the results of this test.
|
||||
$student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
|
||||
$teacher1 = $this->getDataGenerator()->create_user(['username' => 'teacher1']);
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$course1assignment1 = $this->getDataGenerator()->create_module('assignment',
|
||||
[
|
||||
'course' => $course->id,
|
||||
'name' => 'Course 1 - Assignment 1 (onlinetext)',
|
||||
'assignmenttype' => 'onlinetext',
|
||||
]
|
||||
);
|
||||
// Create a second assignment in the same course.
|
||||
$course1assignment2 = $this->getDataGenerator()->create_module('assignment',
|
||||
[
|
||||
'course' => $course->id,
|
||||
'name' => 'Course 1 - Assignment 1 (onlinetext)',
|
||||
'assignmenttype' => 'onlinetext',
|
||||
]
|
||||
);
|
||||
|
||||
$this->add_assignment_submission(
|
||||
$course1assignment1,
|
||||
$student1,
|
||||
"Course 1 - Ass 1: Student1 Test Submission"
|
||||
);
|
||||
|
||||
$this->add_assignment_submission(
|
||||
$course1assignment1,
|
||||
$student2,
|
||||
"Course 1 - Ass 1: Student2 Test Submission"
|
||||
);
|
||||
// Add a submission for the second assignment.
|
||||
$this->add_assignment_submission(
|
||||
$course1assignment2,
|
||||
$student3,
|
||||
"Course 1 - Ass 2: Student3 Test Submission"
|
||||
);
|
||||
|
||||
$submissions = $this->get_course_assignment_submissions($course->id);
|
||||
foreach ($submissions as $submission) {
|
||||
$this->mark_assignment_submission($submission->assignment, $submission->id, $teacher1, 50);
|
||||
}
|
||||
|
||||
$c1ass1ctx = context_module::instance($course1assignment1->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($c1ass1ctx, 'mod_assignment');
|
||||
|
||||
provider::get_users_in_context($userlist);
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
// This is both students and the teacher who marked both assignments.
|
||||
$this->assertCount(3, $userids);
|
||||
// Make sure that student 3 is not in the returned userids.
|
||||
$this->assertFalse(in_array($student3->id, $userids));
|
||||
// Try with the course context.
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$userlist = new \core_privacy\local\request\userlist($coursecontext, 'mod_assignment');
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertEmpty($userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::export_user_data().
|
||||
*
|
||||
@@ -363,6 +433,73 @@ class mod_assignment_privacy_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the deletion of a data for users.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$student1 = $this->getDataGenerator()->create_user(['username' => 'student1']);
|
||||
$student2 = $this->getDataGenerator()->create_user(['username' => 'student2']);
|
||||
$student3 = $this->getDataGenerator()->create_user(['username' => 'student3']);
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$course1assignment = $this->getDataGenerator()->create_module('assignment',
|
||||
[
|
||||
'course' => $course->id,
|
||||
'name' => 'Course 1 - Assignment 1 (single file upload)',
|
||||
'assignmenttype' => 'uploadsingle'
|
||||
]
|
||||
);
|
||||
$context = context_module::instance($course1assignment->cmid);
|
||||
|
||||
// Student one submission.
|
||||
$this->add_file_assignment_submission(
|
||||
$course1assignment,
|
||||
$student1,
|
||||
"Course 1 - Ass 2: " . $student1->id,
|
||||
'Student' . $student1->id . '-Course1-Ass2'
|
||||
);
|
||||
// Student two submission.
|
||||
$this->add_file_assignment_submission(
|
||||
$course1assignment,
|
||||
$student2,
|
||||
"Course 1 - Ass 2: " . $student2->id,
|
||||
'Student' . $student2->id . '-Course1-Ass2'
|
||||
);
|
||||
// Student three submission to be retained.
|
||||
$this->add_file_assignment_submission(
|
||||
$course1assignment,
|
||||
$student3,
|
||||
"Course 1 - Ass 2: " . $student3->id,
|
||||
'Student' . $student3->id . '-Course1-Ass2'
|
||||
);
|
||||
|
||||
$files = $DB->get_records('files', [
|
||||
'component' => 'mod_assignment',
|
||||
'filearea' => 'submission',
|
||||
'contextid' => $context->id
|
||||
]);
|
||||
$this->assertCount(6, $files);
|
||||
|
||||
$submissions = $this->get_assignment_submissions($context->id);
|
||||
$this->assertCount(3, $submissions);
|
||||
|
||||
$userlist = new \core_privacy\local\request\approved_userlist($context, 'mod_assignment', [$student1->id, $student2->id]);
|
||||
provider::delete_data_for_users($userlist);
|
||||
|
||||
$files = $DB->get_records('files', [
|
||||
'component' => 'mod_assignment',
|
||||
'filearea' => 'submission',
|
||||
'contextid' => $context->id
|
||||
]);
|
||||
$this->assertCount(2, $files);
|
||||
|
||||
$submissions = $this->get_assignment_submissions($context->id);
|
||||
$this->assertCount(1, $submissions);
|
||||
}
|
||||
|
||||
// Start of helper functions.
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,9 +33,11 @@ use moodle_recordset;
|
||||
use stdClass;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
/**
|
||||
@@ -48,6 +50,7 @@ use core_privacy\local\request\writer;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -123,6 +126,33 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'instanceid' => $context->instanceid,
|
||||
'modulename' => 'chat',
|
||||
];
|
||||
|
||||
$sql = "SELECT chm.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {chat} c ON c.id = cm.instance
|
||||
JOIN {chat_messages} chm ON chm.chatid = c.id
|
||||
WHERE cm.id = :instanceid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -225,6 +255,28 @@ class provider implements
|
||||
$DB->delete_records_select('chat_users', $sql, $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
|
||||
$chat = $DB->get_record('chat', ['id' => $cm->instance]);
|
||||
|
||||
list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
|
||||
$params = array_merge(['chatid' => $chat->id], $userinparams);
|
||||
$sql = "chatid = :chatid AND userid {$userinsql}";
|
||||
|
||||
$DB->delete_records_select('chat_messages', $sql, $params);
|
||||
$DB->delete_records_select('chat_messages_current', $sql, $params);
|
||||
$DB->delete_records_select('chat_users', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a dict of chat IDs mapped to their course module ID.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,7 @@ global $CFG;
|
||||
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use mod_chat\privacy\provider;
|
||||
@@ -96,6 +97,71 @@ class mod_chat_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue(in_array(context_module::instance($chat2a->cmid)->id, $contextids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users with relevant contexts are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'mod_chat';
|
||||
$dg = $this->getDataGenerator();
|
||||
$c1 = $dg->create_course();
|
||||
$c2 = $dg->create_course();
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
|
||||
$chat1a = $dg->create_module('chat', ['course' => $c1]);
|
||||
$chat1b = $dg->create_module('chat', ['course' => $c1]);
|
||||
$chat2a = $dg->create_module('chat', ['course' => $c2]);
|
||||
|
||||
// Logins but no message.
|
||||
$chatuser = $this->login_user_in_course_chat($u1, $c1, $chat1a);
|
||||
|
||||
// Logins and messages.
|
||||
$chatuser = $this->login_user_in_course_chat($u1, $c1, $chat1b);
|
||||
chat_send_chatmessage($chatuser, 'Hello world!');
|
||||
|
||||
// Silent login (no system message).
|
||||
$chatuser = $this->login_user_in_course_chat($u1, $c2, $chat2a, 0, true);
|
||||
|
||||
// Silent login and messages.
|
||||
$chatuser = $this->login_user_in_course_chat($u2, $c1, $chat1b, 0, true);
|
||||
chat_send_chatmessage($chatuser, 'Ça va ?');
|
||||
chat_send_chatmessage($chatuser, 'Moi, ça va.');
|
||||
|
||||
// Silent login and messages.
|
||||
$chatuser = $this->login_user_in_course_chat($u2, $c2, $chat2a);
|
||||
chat_send_chatmessage($chatuser, 'What\'s happening here?');
|
||||
|
||||
$context1a = context_module::instance($chat1a->cmid);
|
||||
$context1b = context_module::instance($chat1b->cmid);
|
||||
$context2a = context_module::instance($chat2a->cmid);
|
||||
|
||||
$userlist1a = new \core_privacy\local\request\userlist($context1a, $component);
|
||||
$userlist1b = new \core_privacy\local\request\userlist($context1b, $component);
|
||||
$userlist2a = new \core_privacy\local\request\userlist($context2a, $component);
|
||||
\mod_chat\privacy\provider::get_users_in_context($userlist1a);
|
||||
\mod_chat\privacy\provider::get_users_in_context($userlist1b);
|
||||
\mod_chat\privacy\provider::get_users_in_context($userlist2a);
|
||||
|
||||
// Ensure correct users are found in relevant contexts.
|
||||
$this->assertCount(1, $userlist1a);
|
||||
$expected = [$u1->id];
|
||||
$actual = $userlist1a->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$this->assertCount(2, $userlist1b);
|
||||
$expected = [$u1->id, $u2->id];
|
||||
$actual = $userlist1b->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$this->assertCount(1, $userlist2a);
|
||||
$expected = [$u1->id];
|
||||
$actual = $userlist1a->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function test_delete_data_for_all_users_in_context() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
@@ -190,6 +256,62 @@ class mod_chat_privacy_testcase extends provider_testcase {
|
||||
$this->assert_has_no_data_in_chat($u2, $chat1b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$component = 'mod_chat';
|
||||
$dg = $this->getDataGenerator();
|
||||
$c1 = $dg->create_course();
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$u3 = $dg->create_user();
|
||||
|
||||
$chat1 = $dg->create_module('chat', ['course' => $c1]);
|
||||
$chat1context = context_module::instance($chat1->cmid);
|
||||
|
||||
$u1chat1 = $this->login_user_in_course_chat($u1, $c1, $chat1);
|
||||
$u2chat1 = $this->login_user_in_course_chat($u2, $c1, $chat1);
|
||||
$u3chat1 = $this->login_user_in_course_chat($u3, $c1, $chat1);
|
||||
chat_send_chatmessage($u1chat1, 'Ça va ?');
|
||||
chat_send_chatmessage($u2chat1, 'Oui, et toi ?');
|
||||
chat_send_chatmessage($u1chat1, 'Bien merci.');
|
||||
chat_send_chatmessage($u2chat1, 'Pourquoi ils disent omelette "du" fromage ?!');
|
||||
chat_send_chatmessage($u1chat1, 'Aucune idée');
|
||||
chat_send_chatmessage($u3chat1, 'Je ne comprends pas');
|
||||
$this->assert_has_data_in_chat($u1, $chat1);
|
||||
$this->assert_has_data_in_chat($u2, $chat1);
|
||||
$this->assert_has_data_in_chat($u3, $chat1);
|
||||
|
||||
$chat2 = $dg->create_module('chat', ['course' => $c1]);
|
||||
|
||||
$u1chat2 = $this->login_user_in_course_chat($u1, $c1, $chat2);
|
||||
$u2chat2 = $this->login_user_in_course_chat($u2, $c1, $chat2);
|
||||
$u3chat2 = $this->login_user_in_course_chat($u3, $c1, $chat2);
|
||||
chat_send_chatmessage($u1chat2, 'Why do we have a separate chat?');
|
||||
chat_send_chatmessage($u2chat2, 'I have no idea!');
|
||||
chat_send_chatmessage($u3chat2, 'Me either.');
|
||||
$this->assert_has_data_in_chat($u1, $chat2);
|
||||
$this->assert_has_data_in_chat($u2, $chat2);
|
||||
$this->assert_has_data_in_chat($u3, $chat2);
|
||||
|
||||
// Delete user 1 and 2 data from chat 1 context only.
|
||||
$approveduserids = [$u1->id, $u2->id];
|
||||
$approvedlist = new approved_userlist($chat1context, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Ensure correct chat data is deleted.
|
||||
$this->assert_has_no_data_in_chat($u1, $chat1);
|
||||
$this->assert_has_no_data_in_chat($u2, $chat1);
|
||||
$this->assert_has_data_in_chat($u3, $chat1);
|
||||
|
||||
$this->assert_has_data_in_chat($u1, $chat2);
|
||||
$this->assert_has_data_in_chat($u2, $chat2);
|
||||
$this->assert_has_data_in_chat($u3, $chat2);
|
||||
}
|
||||
|
||||
public function test_export_data_for_user() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
|
||||
@@ -26,9 +26,11 @@ namespace mod_data\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\manager;
|
||||
|
||||
@@ -45,6 +47,9 @@ class provider implements
|
||||
// This plugin stores personal data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
|
||||
// This plugin is a core_user_data_provider.
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
@@ -126,6 +131,72 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find users with data records.
|
||||
$sql = "SELECT dr.userid
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'modname' => 'data',
|
||||
'contextid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
];
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Find users with comments.
|
||||
$sql = "SELECT dr.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'modname' => 'data',
|
||||
'contextid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
];
|
||||
|
||||
\core_comment\privacy\provider::get_users_in_context_from_sql(
|
||||
$userlist, 'com', 'mod_data', 'database_entry', $sql, $params);
|
||||
|
||||
// Find users with ratings.
|
||||
$sql = "SELECT dr.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'modname' => 'data',
|
||||
'contextid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
];
|
||||
|
||||
\core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_data', 'entry', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an object from all fields in the $record where key starts with $prefix
|
||||
*
|
||||
@@ -385,6 +456,51 @@ class provider implements
|
||||
// We do not delete ratings made by this user on other records because it may change grades.
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$recordstobedeleted = [];
|
||||
list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
|
||||
|
||||
$sql = "SELECT " . self::sql_fields() . "
|
||||
FROM {context} ctx
|
||||
JOIN {course_modules} cm ON cm.id = ctx.instanceid
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {data} d ON d.id = cm.instance
|
||||
JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid {$userinsql}
|
||||
LEFT JOIN {data_content} dc ON dc.recordid = dr.id
|
||||
LEFT JOIN {data_fields} df ON df.id = dc.fieldid
|
||||
WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel
|
||||
ORDER BY dr.id";
|
||||
|
||||
$params = [
|
||||
'ctxid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'data',
|
||||
];
|
||||
$params += $userinparams;
|
||||
|
||||
$rs = $DB->get_recordset_sql($sql, $params);
|
||||
foreach ($rs as $row) {
|
||||
self::mark_data_content_for_deletion($context, $row);
|
||||
$recordstobedeleted[$row->recordid] = $row->recordid;
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
self::delete_data_records($context, $recordstobedeleted);
|
||||
|
||||
// Additionally remove comments these users made on other entries.
|
||||
\core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_data', 'database_entry');
|
||||
|
||||
// We do not delete ratings made by users on other records because it may change grades.
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a data_record/data_content for deletion
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use mod_data\privacy\provider;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -39,6 +40,8 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te
|
||||
protected $student;
|
||||
/** @var stdClass The student object. */
|
||||
protected $student2;
|
||||
/** @var stdClass The student object. */
|
||||
protected $student3;
|
||||
|
||||
/** @var stdClass The data object. */
|
||||
protected $datamodule;
|
||||
@@ -84,9 +87,11 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te
|
||||
// Create a student.
|
||||
$student1 = $generator->create_user();
|
||||
$student2 = $generator->create_user();
|
||||
$student3 = $generator->create_user();
|
||||
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
|
||||
$generator->enrol_user($student1->id, $course->id, $studentrole->id);
|
||||
$generator->enrol_user($student2->id, $course->id, $studentrole->id);
|
||||
$generator->enrol_user($student3->id, $course->id, $studentrole->id);
|
||||
|
||||
// Add records.
|
||||
$this->setUser($student1);
|
||||
@@ -98,8 +103,12 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te
|
||||
$this->generate_data_record($datamodule);
|
||||
$this->generate_data_record($datamodule);
|
||||
|
||||
$this->setUser($student3);
|
||||
$this->generate_data_record($datamodule);
|
||||
|
||||
$this->student = $student1;
|
||||
$this->student2 = $student2;
|
||||
$this->student3 = $student3;
|
||||
$this->datamodule = $datamodule;
|
||||
$this->course = $course;
|
||||
}
|
||||
@@ -177,6 +186,27 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te
|
||||
$this->assertEquals($cmcontext->id, $contextforuser->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'mod_data';
|
||||
$cm = get_coursemodule_from_instance('data', $this->datamodule->id);
|
||||
$cmcontext = context_module::instance($cm->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($cmcontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(3, $userlist);
|
||||
|
||||
$expected = [$this->student->id, $this->student2->id, $this->student3->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test privacy writer
|
||||
*
|
||||
@@ -240,4 +270,46 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te
|
||||
provider::export_user_data($appctxt);
|
||||
$this->assertFalse($this->get_writer($cmcontext)->has_any_data());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$cm = get_coursemodule_from_instance('data', $this->datamodule->id);
|
||||
$cmcontext = context_module::instance($cm->id);
|
||||
$userstodelete = [$this->student->id, $this->student2->id];
|
||||
|
||||
// Ensure student, student 2 and student 3 have data before being deleted.
|
||||
$appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]);
|
||||
provider::export_user_data($appctxt);
|
||||
$this->assertTrue($this->get_writer($cmcontext)->has_any_data());
|
||||
|
||||
$appctxt = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_data', [$cmcontext->id]);
|
||||
provider::export_user_data($appctxt);
|
||||
$this->assertTrue($this->get_writer($cmcontext)->has_any_data());
|
||||
|
||||
// Delete data for student 1 and 2.
|
||||
$approvedlist = new approved_userlist($cmcontext, 'mod_data', $userstodelete);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// Reset the writer so it doesn't contain the data from before deletion.
|
||||
\core_privacy\local\request\writer::reset();
|
||||
|
||||
// Ensure data is now deleted for student and student 2.
|
||||
$appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]);
|
||||
provider::export_user_data($appctxt);
|
||||
|
||||
$this->assertFalse($this->get_writer($cmcontext)->has_any_data());
|
||||
|
||||
$appctxt = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_data', [$cmcontext->id]);
|
||||
provider::export_user_data($appctxt);
|
||||
|
||||
$this->assertFalse($this->get_writer($cmcontext)->has_any_data());
|
||||
|
||||
// Ensure data still intact for student 3.
|
||||
$appctxt = new \core_privacy\local\request\approved_contextlist($this->student3, 'mod_data', [$cmcontext->id]);
|
||||
provider::export_user_data($appctxt);
|
||||
|
||||
$this->assertTrue($this->get_writer($cmcontext)->has_any_data());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,11 @@ use context_helper;
|
||||
use stdClass;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
require_once($CFG->dirroot . '/mod/feedback/lib.php');
|
||||
@@ -48,6 +50,7 @@ require_once($CFG->dirroot . '/mod/feedback/lib.php');
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -102,6 +105,38 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find users with feedback entries.
|
||||
$sql = "
|
||||
SELECT fc.userid
|
||||
FROM {%s} fc
|
||||
JOIN {modules} m
|
||||
ON m.name = :feedback
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = fc.feedback
|
||||
AND cm.module = m.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = cm.id
|
||||
AND ctx.contextlevel = :modlevel
|
||||
WHERE ctx.id = :contextid";
|
||||
$params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
|
||||
|
||||
$userlist->add_from_sql('userid', sprintf($sql, 'feedback_completed'), $params);
|
||||
$userlist->add_from_sql('userid', sprintf($sql, 'feedback_completedtmp'), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -272,6 +307,48 @@ class provider implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
// Prepare SQL to gather all completed IDs.
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$completedsql = "
|
||||
SELECT fc.id
|
||||
FROM {%s} fc
|
||||
JOIN {modules} m
|
||||
ON m.name = :feedback
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = fc.feedback
|
||||
AND cm.module = m.id
|
||||
WHERE cm.id = :instanceid
|
||||
AND fc.userid $insql";
|
||||
$completedparams = array_merge($inparams, ['instanceid' => $context->instanceid, 'feedback' => 'feedback']);
|
||||
|
||||
// Delete all submissions in progress.
|
||||
$completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
|
||||
if (!empty($completedtmpids)) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
|
||||
$DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
|
||||
$DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
|
||||
}
|
||||
|
||||
// Delete all final submissions.
|
||||
$completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
|
||||
if (!empty($completedids)) {
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
|
||||
$DB->delete_records_select('feedback_value', "completed $insql", $inparams);
|
||||
$DB->delete_records_select('feedback_completed', "id $insql", $inparams);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an item record from a database record.
|
||||
*
|
||||
|
||||
@@ -115,6 +115,83 @@ class mod_feedback_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue(in_array(context_module::instance($cm2c->cmid)->id, $contextids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting the users in a context.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
$fg = $dg->get_plugin_generator('mod_feedback');
|
||||
$component = 'mod_feedback';
|
||||
|
||||
$c1 = $dg->create_course();
|
||||
$c2 = $dg->create_course();
|
||||
$cm0 = $dg->create_module('feedback', ['course' => SITEID]);
|
||||
$cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]);
|
||||
$cm1b = $dg->create_module('feedback', ['course' => $c1]);
|
||||
$cm2 = $dg->create_module('feedback', ['course' => $c2]);
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
|
||||
foreach ([$cm0, $cm1a, $cm1b, $cm2] as $feedback) {
|
||||
$i1 = $fg->create_item_numeric($feedback);
|
||||
$i2 = $fg->create_item_multichoice($feedback);
|
||||
$answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]];
|
||||
|
||||
if ($feedback == $cm1b) {
|
||||
$this->create_submission_with_answers($feedback, $u2, $answers);
|
||||
} else {
|
||||
$this->create_submission_with_answers($feedback, $u1, $answers);
|
||||
}
|
||||
}
|
||||
|
||||
// Unsaved submission for u2 in cm1a.
|
||||
$feedback = $cm1a;
|
||||
$i1 = $fg->create_item_numeric($feedback);
|
||||
$i2 = $fg->create_item_multichoice($feedback);
|
||||
$answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]];
|
||||
$this->create_tmp_submission_with_answers($feedback, $u2, $answers);
|
||||
|
||||
// Only u1 in cm0.
|
||||
$context = context_module::instance($cm0->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals([$u1->id], $userlist->get_userids());
|
||||
|
||||
$context = context_module::instance($cm1a->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
// Two submissions in cm1a: saved for u1, unsaved for u2.
|
||||
$this->assertCount(2, $userlist);
|
||||
|
||||
$expected = [$u1->id, $u2->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Only u2 in cm1b.
|
||||
$context = context_module::instance($cm1b->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals([$u2->id], $userlist->get_userids());
|
||||
|
||||
// Only u1 in cm2.
|
||||
$context = context_module::instance($cm2->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals([$u1->id], $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting user data.
|
||||
*/
|
||||
@@ -169,6 +246,66 @@ class mod_feedback_privacy_testcase extends provider_testcase {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting data within a context for an approved userlist.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
$fg = $dg->get_plugin_generator('mod_feedback');
|
||||
|
||||
$c1 = $dg->create_course();
|
||||
$c2 = $dg->create_course();
|
||||
$cm0 = $dg->create_module('feedback', ['course' => SITEID]);
|
||||
$cm1 = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]);
|
||||
$cm2 = $dg->create_module('feedback', ['course' => $c2]);
|
||||
$context0 = context_module::instance($cm0->cmid);
|
||||
$context1 = context_module::instance($cm1->cmid);
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
|
||||
// Create a bunch of data.
|
||||
foreach ([$cm0, $cm1, $cm2] as $feedback) {
|
||||
$i1 = $fg->create_item_numeric($feedback);
|
||||
$i2 = $fg->create_item_multichoice($feedback);
|
||||
$answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]];
|
||||
|
||||
$this->create_submission_with_answers($feedback, $u1, $answers);
|
||||
$this->create_tmp_submission_with_answers($feedback, $u1, $answers);
|
||||
|
||||
$this->create_submission_with_answers($feedback, $u2, $answers);
|
||||
$this->create_tmp_submission_with_answers($feedback, $u2, $answers);
|
||||
}
|
||||
|
||||
// Delete u1 from cm0, ensure u2 data is retained.
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($context0, 'mod_feedback', [$u1->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
$this->assert_no_feedback_data_for_user($cm0, $u1);
|
||||
$this->assert_feedback_data_for_user($cm0, $u2);
|
||||
$this->assert_feedback_tmp_data_for_user($cm0, $u2);
|
||||
|
||||
// Ensure cm1 unaffected by cm1 deletes.
|
||||
$this->assert_feedback_data_for_user($cm1, $u1);
|
||||
$this->assert_feedback_tmp_data_for_user($cm1, $u1);
|
||||
$this->assert_feedback_data_for_user($cm1, $u2);
|
||||
$this->assert_feedback_tmp_data_for_user($cm1, $u2);
|
||||
|
||||
// Delete u1 and u2 from cm1, ensure no data is retained.
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($context1, 'mod_feedback', [$u1->id, $u2->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
$this->assert_no_feedback_data_for_user($cm1, $u1);
|
||||
$this->assert_no_feedback_data_for_user($cm1, $u2);
|
||||
|
||||
// Ensure cm2 is unaffected by any of the deletes.
|
||||
$this->assert_feedback_data_for_user($cm2, $u1);
|
||||
$this->assert_feedback_tmp_data_for_user($cm2, $u1);
|
||||
$this->assert_feedback_data_for_user($cm2, $u2);
|
||||
$this->assert_feedback_tmp_data_for_user($cm2, $u2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting a whole context.
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
|
||||
namespace mod_forum\privacy;
|
||||
|
||||
use \core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_contextlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \core_privacy\local\request\deletion_criteria;
|
||||
use \core_privacy\local\request\writer;
|
||||
use \core_privacy\local\request\helper as request_helper;
|
||||
@@ -46,6 +48,9 @@ class provider implements
|
||||
// This plugin currently implements the original plugin\provider interface.
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
|
||||
// This plugin has some sitewide user preferences to export.
|
||||
\core_privacy\local\request\user_preference_provider
|
||||
{
|
||||
@@ -144,7 +149,7 @@ class provider implements
|
||||
* In the case of forum, that is any forum where the user has made any post, rated any content, or has any preferences.
|
||||
*
|
||||
* @param int $userid The user to search.
|
||||
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
|
||||
*/
|
||||
public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
|
||||
$ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
|
||||
@@ -192,6 +197,98 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'instanceid' => $context->instanceid,
|
||||
'modulename' => 'forum',
|
||||
];
|
||||
|
||||
// Discussion authors.
|
||||
$sql = "SELECT d.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussions} d ON d.forum = f.id
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Forum authors.
|
||||
$sql = "SELECT p.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussions} d ON d.forum = f.id
|
||||
JOIN {forum_posts} p ON d.id = p.discussion
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Forum post ratings.
|
||||
$sql = "SELECT p.id
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussions} d ON d.forum = f.id
|
||||
JOIN {forum_posts} p ON d.id = p.discussion
|
||||
WHERE cm.id = :instanceid";
|
||||
\core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_forum', 'post', $sql, $params);
|
||||
|
||||
// Forum Digest settings.
|
||||
$sql = "SELECT dig.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_digests} dig ON dig.forum = f.id
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Forum Subscriptions.
|
||||
$sql = "SELECT sub.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_subscriptions} sub ON sub.forum = f.id
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Discussion subscriptions.
|
||||
$sql = "SELECT dsub.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Read Posts.
|
||||
$sql = "SELECT hasread.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_read} hasread ON hasread.forumid = f.id
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Tracking Preferences.
|
||||
$sql = "SELECT pref.userid
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
|
||||
JOIN {forum} f ON f.id = cm.instance
|
||||
JOIN {forum_track_prefs} pref ON pref.forumid = f.id
|
||||
WHERE cm.id = :instanceid";
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store all user preferences for the plugin.
|
||||
*
|
||||
@@ -887,4 +984,58 @@ class provider implements
|
||||
$fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $postparams);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
|
||||
$forum = $DB->get_record('forum', ['id' => $cm->instance]);
|
||||
|
||||
list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
|
||||
$params = array_merge(['forumid' => $forum->id], $userinparams);
|
||||
|
||||
$DB->delete_records_select('forum_track_prefs', "forumid = :forumid AND userid {$userinsql}", $params);
|
||||
$DB->delete_records_select('forum_subscriptions', "forum = :forumid AND userid {$userinsql}", $params);
|
||||
$DB->delete_records_select('forum_read', "forumid = :forumid AND userid {$userinsql}", $params);
|
||||
$DB->delete_records_select(
|
||||
'forum_queue',
|
||||
"userid {$userinsql} AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)",
|
||||
$params
|
||||
);
|
||||
$DB->delete_records_select('forum_discussion_subs', "forum = :forumid AND userid {$userinsql}", $params);
|
||||
|
||||
// Do not delete discussion or forum posts.
|
||||
// Instead update them to reflect that the content has been deleted.
|
||||
$postsql = "userid {$userinsql} AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)";
|
||||
$postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}";
|
||||
|
||||
// Update the subject.
|
||||
$DB->set_field_select('forum_posts', 'subject', '', $postsql, $params);
|
||||
|
||||
// Update the subject and its format.
|
||||
$DB->set_field_select('forum_posts', 'message', '', $postsql, $params);
|
||||
$DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $params);
|
||||
|
||||
// Mark the post as deleted.
|
||||
$DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $params);
|
||||
|
||||
// Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts.
|
||||
// Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating
|
||||
// of any post.
|
||||
\core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $params);
|
||||
|
||||
// Delete all Tags.
|
||||
\core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $params);
|
||||
|
||||
// Delete all files from the posts.
|
||||
$fs = get_file_storage();
|
||||
$fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $params);
|
||||
$fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1388,4 +1388,519 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
|
||||
// Files for the other posts should remain.
|
||||
$this->assertCount(18, $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that user data for specific users is deleted from a specified context.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
|
||||
$fs = get_file_storage();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$users = $this->helper_create_users($course, 5);
|
||||
|
||||
$forums = [];
|
||||
$contexts = [];
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$forum = $this->getDataGenerator()->create_module('forum', [
|
||||
'course' => $course->id,
|
||||
'scale' => 100,
|
||||
]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
$forums[$forum->id] = $forum;
|
||||
$contexts[$forum->id] = $context;
|
||||
}
|
||||
|
||||
$discussions = [];
|
||||
$posts = [];
|
||||
$postsbyforum = [];
|
||||
foreach ($users as $user) {
|
||||
$postsbyforum[$user->id] = [];
|
||||
foreach ($forums as $forum) {
|
||||
$context = $contexts[$forum->id];
|
||||
|
||||
// Create a new discussion + post in the forum.
|
||||
list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
|
||||
$discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
|
||||
$discussions[$discussion->id] = $discussion;
|
||||
$postsbyforum[$user->id][$context->id] = [];
|
||||
|
||||
// Add a number of replies.
|
||||
$posts[$post->id] = $post;
|
||||
$thisforumposts[$post->id] = $post;
|
||||
$postsbyforum[$user->id][$context->id][$post->id] = $post;
|
||||
|
||||
$reply = $this->helper_reply_to_post($post, $user);
|
||||
$posts[$reply->id] = $reply;
|
||||
$postsbyforum[$user->id][$context->id][$reply->id] = $reply;
|
||||
|
||||
$reply = $this->helper_reply_to_post($post, $user);
|
||||
$posts[$reply->id] = $reply;
|
||||
$postsbyforum[$user->id][$context->id][$reply->id] = $reply;
|
||||
|
||||
$reply = $this->helper_reply_to_post($reply, $user);
|
||||
$posts[$reply->id] = $reply;
|
||||
$postsbyforum[$user->id][$context->id][$reply->id] = $reply;
|
||||
|
||||
// Add a fake inline image to the original post.
|
||||
$fs->create_file_from_string([
|
||||
'contextid' => $context->id,
|
||||
'component' => 'mod_forum',
|
||||
'filearea' => 'post',
|
||||
'itemid' => $post->id,
|
||||
'filepath' => '/',
|
||||
'filename' => 'example.jpg',
|
||||
], 'image contents (not really)');
|
||||
// And a fake attachment.
|
||||
$fs->create_file_from_string([
|
||||
'contextid' => $context->id,
|
||||
'component' => 'mod_forum',
|
||||
'filearea' => 'attachment',
|
||||
'itemid' => $post->id,
|
||||
'filepath' => '/',
|
||||
'filename' => 'example.jpg',
|
||||
], 'image contents (not really)');
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all posts as read by user1.
|
||||
$user1 = reset($users);
|
||||
foreach ($posts as $post) {
|
||||
$discussion = $discussions[$post->discussion];
|
||||
$forum = $forums[$discussion->forum];
|
||||
$context = $contexts[$forum->id];
|
||||
|
||||
// Mark the post as being read by user1.
|
||||
forum_tp_add_read_record($user1->id, $post->id);
|
||||
}
|
||||
|
||||
// Rate and tag all posts.
|
||||
$ratedposts = [];
|
||||
foreach ($users as $user) {
|
||||
foreach ($posts as $post) {
|
||||
$discussion = $discussions[$post->discussion];
|
||||
$forum = $forums[$discussion->forum];
|
||||
$context = $contexts[$forum->id];
|
||||
|
||||
// Tag the post.
|
||||
\core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
|
||||
|
||||
// Rate the other users content.
|
||||
if ($post->userid != $user->id) {
|
||||
$ratedposts[$post->id] = $post;
|
||||
$rm = new rating_manager();
|
||||
$ratingoptions = (object) [
|
||||
'context' => $context,
|
||||
'component' => 'mod_forum',
|
||||
'ratingarea' => 'post',
|
||||
'itemid' => $post->id,
|
||||
'scaleid' => $forum->scale,
|
||||
'userid' => $user->id,
|
||||
];
|
||||
|
||||
$rating = new \rating($ratingoptions);
|
||||
$rating->update_rating(75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete for one of the forums for the first user.
|
||||
$firstcontext = reset($contexts);
|
||||
|
||||
$deletedpostids = [];
|
||||
$otherpostids = [];
|
||||
foreach ($postsbyforum as $user => $contexts) {
|
||||
foreach ($contexts as $thiscontextid => $theseposts) {
|
||||
$thesepostids = array_map(function($post) {
|
||||
return $post->id;
|
||||
}, $theseposts);
|
||||
|
||||
if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
|
||||
// This post is in the deleted context and by the target user.
|
||||
$deletedpostids = array_merge($deletedpostids, $thesepostids);
|
||||
} else {
|
||||
// This post is by another user, or in a non-target context.
|
||||
$otherpostids = array_merge($otherpostids, $thesepostids);
|
||||
}
|
||||
}
|
||||
}
|
||||
list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
|
||||
list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
|
||||
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($firstcontext, 'mod_forum', [$user1->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
// All posts should remain.
|
||||
$this->assertCount(40, $DB->get_records('forum_posts'));
|
||||
|
||||
// There should be 8 posts belonging to user1.
|
||||
$this->assertCount(8, $DB->get_records('forum_posts', [
|
||||
'userid' => $user1->id,
|
||||
]));
|
||||
|
||||
// Four of those posts should have been marked as deleted.
|
||||
// That means that the deleted flag is set, and both the subject and message are empty.
|
||||
$this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
|
||||
. " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
|
||||
. " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
|
||||
, [
|
||||
'userid' => $user1->id,
|
||||
'deleted' => 1,
|
||||
'subject' => '',
|
||||
'message' => '',
|
||||
]));
|
||||
|
||||
// Only user1's posts should have been marked this way.
|
||||
$this->assertCount(4, $DB->get_records('forum_posts', [
|
||||
'deleted' => 1,
|
||||
]));
|
||||
$this->assertCount(4, $DB->get_records_select('forum_posts',
|
||||
$DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
|
||||
'subject' => '',
|
||||
]));
|
||||
$this->assertCount(4, $DB->get_records_select('forum_posts',
|
||||
$DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
|
||||
'message' => '',
|
||||
]));
|
||||
|
||||
// Only the posts in the first discussion should have been marked this way.
|
||||
$this->assertCount(4, $DB->get_records_select('forum_posts',
|
||||
"deleted = :deleted AND id {$postinsql}",
|
||||
array_merge($postinparams, [
|
||||
'deleted' => 1,
|
||||
])
|
||||
));
|
||||
|
||||
// Ratings should have been removed from the affected posts.
|
||||
$this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
|
||||
|
||||
// Ratings should remain on posts in the other context, and posts not belonging to the affected user.
|
||||
$this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
|
||||
|
||||
// Ratings should remain where the user has rated another person's post.
|
||||
$this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
|
||||
|
||||
// Tags for the affected posts should be removed.
|
||||
$this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
|
||||
|
||||
// Tags should remain for the other posts by this user, and all posts by other users.
|
||||
$this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
|
||||
|
||||
// Files for the affected posts should be removed.
|
||||
// 5 users * 2 forums * 1 file in each forum
|
||||
// Original total: 10
|
||||
// One post with file removed.
|
||||
$this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams));
|
||||
|
||||
// Files for the other posts should remain.
|
||||
$this->assertCount(18,
|
||||
$DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the discussion author is listed as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_post_author() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
list($author, $user) = $this->helper_create_users($course, 2);
|
||||
|
||||
list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// There should only be one user in the list.
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals([$author->id], $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all post authors are included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_post_authors() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
list($author, $user, $other) = $this->helper_create_users($course, 3);
|
||||
|
||||
list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
|
||||
$fp1reply = $this->helper_post_to_discussion($forum, $fd1, $user);
|
||||
$fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// Two users - author and replier.
|
||||
$this->assertCount(2, $userlist);
|
||||
|
||||
$expected = [$author->id, $user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all post raters are included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_post_ratings() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
list($author, $user, $other) = $this->helper_create_users($course, 3);
|
||||
|
||||
list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
|
||||
|
||||
// Rate the other users content.
|
||||
$rm = new rating_manager();
|
||||
$ratingoptions = (object) [
|
||||
'context' => $context,
|
||||
'component' => 'mod_forum',
|
||||
'ratingarea' => 'post',
|
||||
'itemid' => $fp1->id,
|
||||
'scaleid' => $forum->scale,
|
||||
'userid' => $user->id,
|
||||
];
|
||||
|
||||
$rating = new \rating($ratingoptions);
|
||||
$rating->update_rating(75);
|
||||
|
||||
$fp1reply = $this->helper_post_to_discussion($forum, $fd1, $author);
|
||||
$fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// Two users - author and rater.
|
||||
$this->assertCount(2, $userlist);
|
||||
|
||||
$expected = [$author->id, $user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all users with a digest preference are included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_digest_preference() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
$otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$othercm = get_coursemodule_from_instance('forum', $otherforum->id);
|
||||
$othercontext = \context_module::instance($othercm->id);
|
||||
|
||||
list($user, $otheruser) = $this->helper_create_users($course, 2);
|
||||
|
||||
// Add digest subscriptions.
|
||||
forum_set_user_maildigest($forum, 0, $user);
|
||||
forum_set_user_maildigest($otherforum, 0, $otheruser);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// One user - the one with a digest preference.
|
||||
$this->assertCount(1, $userlist);
|
||||
|
||||
$expected = [$user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all users with a forum subscription preference included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_with_subscription() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
$otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$othercm = get_coursemodule_from_instance('forum', $otherforum->id);
|
||||
$othercontext = \context_module::instance($othercm->id);
|
||||
|
||||
list($user, $otheruser) = $this->helper_create_users($course, 2);
|
||||
|
||||
// Subscribe the user to the forum.
|
||||
\mod_forum\subscriptions::subscribe_user($user->id, $forum);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// One user - the one with a digest preference.
|
||||
$this->assertCount(1, $userlist);
|
||||
|
||||
$expected = [$user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all users with a per-discussion subscription preference included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_with_discussion_subscription() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
$otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$othercm = get_coursemodule_from_instance('forum', $otherforum->id);
|
||||
$othercontext = \context_module::instance($othercm->id);
|
||||
|
||||
list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
|
||||
|
||||
// Post in both of the forums.
|
||||
list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
|
||||
list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author);
|
||||
|
||||
// Subscribe the user to the discussions.
|
||||
\mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $fd1);
|
||||
\mod_forum\subscriptions::subscribe_user_to_discussion($otheruser->id, $ofd1);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// Two users - the author, and the one who subscribed.
|
||||
$this->assertCount(2, $userlist);
|
||||
|
||||
$expected = [$author->id, $user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all users with read tracking are included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_with_read_post_tracking() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
$otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$othercm = get_coursemodule_from_instance('forum', $otherforum->id);
|
||||
$othercontext = \context_module::instance($othercm->id);
|
||||
|
||||
list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
|
||||
|
||||
// Post in both of the forums.
|
||||
list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
|
||||
list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author);
|
||||
|
||||
// Add read information for those users.
|
||||
forum_tp_add_read_record($user->id, $fp1->id);
|
||||
forum_tp_add_read_record($otheruser->id, $ofp1->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// Two user - the author, and the one who has read the post.
|
||||
$this->assertCount(2, $userlist);
|
||||
|
||||
$expected = [$author->id, $user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all users with tracking preferences are included as a user in the context.
|
||||
*/
|
||||
public function test_get_users_in_context_with_tracking_preferences() {
|
||||
global $DB;
|
||||
$component = 'mod_forum';
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
|
||||
$otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$othercm = get_coursemodule_from_instance('forum', $otherforum->id);
|
||||
$othercontext = \context_module::instance($othercm->id);
|
||||
|
||||
list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
|
||||
|
||||
// Forum tracking is opt-out.
|
||||
// Stop tracking the read posts.
|
||||
forum_tp_stop_tracking($forum->id, $user->id);
|
||||
forum_tp_stop_tracking($otherforum->id, $otheruser->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
\mod_forum\privacy\provider::get_users_in_context($userlist);
|
||||
|
||||
// One user - the one who is tracking that forum.
|
||||
$this->assertCount(1, $userlist);
|
||||
|
||||
$expected = [$user->id];
|
||||
sort($expected);
|
||||
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
namespace mod_glossary\privacy;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\deletion_criteria;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -39,6 +41,8 @@ defined('MOODLE_INTERNAL') || die();
|
||||
class provider implements
|
||||
// This plugin stores personal data.
|
||||
\core_privacy\local\metadata\provider,
|
||||
// This plugin is capable of determining which users have data within it.
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
// This plugin is a core_user_data_provider.
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
@@ -101,6 +105,72 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find users with glossary entries.
|
||||
$sql = "SELECT ge.userid
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {glossary} g ON g.id = cm.instance
|
||||
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'glossary',
|
||||
];
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Find users with glossary comments.
|
||||
$sql = "SELECT ge.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {glossary} g ON g.id = cm.instance
|
||||
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'glossary',
|
||||
];
|
||||
|
||||
\core_comment\privacy\provider::get_users_in_context_from_sql(
|
||||
$userlist, 'com', 'mod_glossary', 'glossary_entry', $sql, $params);
|
||||
|
||||
// Find users with glossary ratings.
|
||||
$sql = "SELECT ge.id
|
||||
FROM {context} c
|
||||
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
|
||||
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
|
||||
JOIN {glossary} g ON g.id = cm.instance
|
||||
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'modname' => 'glossary',
|
||||
];
|
||||
|
||||
\core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_glossary', 'entry', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export personal data for the given approved_contextlist.
|
||||
*
|
||||
@@ -324,4 +394,59 @@ class provider implements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$userids = $userlist->get_userids();
|
||||
$instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
|
||||
list($userinsql, $userinparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
|
||||
$glossaryentrieswhere = "glossaryid = :instanceid AND userid {$userinsql}";
|
||||
$userinstanceparams = $userinparams + ['instanceid' => $instanceid];
|
||||
|
||||
$entriesobject = $DB->get_recordset_select('glossary_entries', $glossaryentrieswhere, $userinstanceparams, 'id', 'id');
|
||||
$entries = [];
|
||||
|
||||
foreach ($entriesobject as $entry) {
|
||||
$entries[] = $entry->id;
|
||||
}
|
||||
|
||||
$entriesobject->close();
|
||||
|
||||
if (!$entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($entries, SQL_PARAMS_NAMED);
|
||||
|
||||
// Delete related entry aliases.
|
||||
$DB->delete_records_list('glossary_alias', 'entryid', $entries);
|
||||
|
||||
// Delete related entry categories.
|
||||
$DB->delete_records_list('glossary_entries_categories', 'entryid', $entries);
|
||||
|
||||
// Delete related entry and attachment files.
|
||||
get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'entry', $insql, $inparams);
|
||||
get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'attachment', $insql, $inparams);
|
||||
|
||||
// Delete user tags related to this glossary.
|
||||
\core_tag\privacy\provider::delete_item_tags_select($context, 'mod_glossary', 'glossary_entries', $insql, $inparams);
|
||||
|
||||
// Delete related ratings.
|
||||
\core_rating\privacy\provider::delete_ratings_select($context, 'mod_glossary', 'entry', $insql, $inparams);
|
||||
|
||||
// Delete comments.
|
||||
\core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_glossary', 'glossary_entry');
|
||||
|
||||
// Now delete all user related entries.
|
||||
$deletewhere = "glossaryid = :instanceid AND userid {$userinsql}";
|
||||
$DB->delete_records_select('glossary_entries', $deletewhere, $userinstanceparams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/comment/lib.php');
|
||||
require_once($CFG->dirroot . '/rating/lib.php');
|
||||
|
||||
/**
|
||||
* Privacy provider tests class.
|
||||
@@ -131,6 +132,27 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
|
||||
$this->assertEquals($cmcontext->id, $contextforuser->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$component = 'mod_glossary';
|
||||
$cm = get_coursemodule_from_instance('glossary', $this->glossary->id);
|
||||
$cmcontext = context_module::instance($cm->id);
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($cmcontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(1, $userlist);
|
||||
|
||||
$expected = [$this->student->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::export_user_data().
|
||||
*/
|
||||
@@ -212,6 +234,7 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
|
||||
global $DB;
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Create another student who will add an entry to the first glossary.
|
||||
$student2 = $generator->create_user();
|
||||
$generator->enrol_user($student2->id, $this->course->id, 'student');
|
||||
|
||||
@@ -235,6 +258,11 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
|
||||
|
||||
core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']);
|
||||
|
||||
// As a teacher, rate student 2's entry.
|
||||
$this->setUser($this->teacher);
|
||||
$rating = $this->get_rating_object($context1, $ge3->id);
|
||||
$rating->update_rating(2);
|
||||
|
||||
// Before deletion, we should have 3 entries, one rating and 2 tag instances.
|
||||
$count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]);
|
||||
$this->assertEquals(3, $count);
|
||||
@@ -243,7 +271,10 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
|
||||
$this->assertEquals(2, $tagcount);
|
||||
$aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge3->id]);
|
||||
$this->assertEquals(1, $aliascount);
|
||||
// Create another student who will add an entry to the first glossary.
|
||||
$ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry',
|
||||
'itemid' => $ge3->id]);
|
||||
$this->assertEquals(1, $ratingcount);
|
||||
|
||||
$contextlist = new \core_privacy\local\request\approved_contextlist($student2, 'glossary',
|
||||
[$context1->id, $context2->id]);
|
||||
provider::delete_data_for_user($contextlist);
|
||||
@@ -274,6 +305,120 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
|
||||
$commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry',
|
||||
'userid' => $this->student->id]);
|
||||
$this->assertEquals(1, $commentcount);
|
||||
|
||||
$ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry',
|
||||
'itemid' => $ge3->id]);
|
||||
$this->assertEquals(0, $ratingcount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$student2 = $generator->create_user();
|
||||
$generator->enrol_user($student2->id, $this->course->id, 'student');
|
||||
|
||||
$cm1 = get_coursemodule_from_instance('glossary', $this->glossary->id);
|
||||
$glossary2 = $this->plugingenerator->create_instance(['course' => $this->course->id]);
|
||||
$cm2 = get_coursemodule_from_instance('glossary', $glossary2->id);
|
||||
|
||||
$ge1 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first user glossary entry', 'approved' => 1]);
|
||||
$ge2 = $this->plugingenerator->create_content($glossary2, ['concept' => 'first user second glossary entry',
|
||||
'approved' => 1], ['two']);
|
||||
|
||||
$context1 = context_module::instance($cm1->id);
|
||||
$context2 = context_module::instance($cm2->id);
|
||||
core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context1, ['Parmi', 'Sushi']);
|
||||
|
||||
$this->setUser($student2);
|
||||
$ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'second user glossary entry',
|
||||
'approved' => 1], ['three']);
|
||||
|
||||
$comment = $this->get_comment_object($context1, $ge3->id);
|
||||
$comment->add('User 2 comment 1');
|
||||
$comment = $this->get_comment_object($context2, $ge2->id);
|
||||
$comment->add('User 2 comment 2');
|
||||
|
||||
core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']);
|
||||
core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge2->id, $context2, ['Potato', 'Kumara']);
|
||||
|
||||
// As a teacher, rate student 2's entry.
|
||||
$this->setUser($this->teacher);
|
||||
$rating = $this->get_rating_object($context1, $ge3->id);
|
||||
$rating->update_rating(2);
|
||||
|
||||
// Check correct glossary 1 record counts before deletion.
|
||||
$count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]);
|
||||
// Note: There is an additional student entry from setUp().
|
||||
$this->assertEquals(3, $count);
|
||||
|
||||
list($context1itemsql, $context1itemparams) = $DB->get_in_or_equal([$ge1->id, $ge3->id], SQL_PARAMS_NAMED);
|
||||
$geparams = [
|
||||
'component' => 'mod_glossary',
|
||||
'itemtype' => 'glossary_entries',
|
||||
];
|
||||
$geparams += $context1itemparams;
|
||||
$wheresql = "component = :component AND itemtype = :itemtype AND itemid {$context1itemsql}";
|
||||
|
||||
$tagcount = $DB->count_records_select('tag_instance', $wheresql, $geparams);
|
||||
$this->assertEquals(4, $tagcount);
|
||||
|
||||
$aliascount = $DB->count_records_select('glossary_alias', "entryid {$context1itemsql}", $context1itemparams);
|
||||
$this->assertEquals(1, $aliascount);
|
||||
|
||||
$commentparams = [
|
||||
'component' => 'mod_glossary',
|
||||
'commentarea' => 'glossary_entry',
|
||||
];
|
||||
$commentparams += $context1itemparams;
|
||||
$commentwhere = "component = :component AND commentarea = :commentarea AND itemid {$context1itemsql}";
|
||||
|
||||
$commentcount = $DB->count_records_select('comments', $commentwhere, $commentparams);
|
||||
$this->assertEquals(1, $commentcount);
|
||||
|
||||
$ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry',
|
||||
'itemid' => $ge3->id]);
|
||||
$this->assertEquals(1, $ratingcount);
|
||||
|
||||
// Perform deletion within context 1 for both students.
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($context1, 'mod_glossary',
|
||||
[$this->student->id, $student2->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
// After deletion, all context 1 entries, tags and comment should be deleted.
|
||||
$count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]);
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
$tagcount = $DB->count_records_select('tag_instance', $wheresql, $geparams);
|
||||
$this->assertEquals(0, $tagcount);
|
||||
|
||||
$aliascount = $DB->count_records_select('glossary_alias', "entryid {$context1itemsql}", $context1itemparams);
|
||||
$this->assertEquals(0, $aliascount);
|
||||
|
||||
$commentcount = $DB->count_records_select('comments', $commentwhere, $commentparams);
|
||||
$this->assertEquals(0, $commentcount);
|
||||
|
||||
// Context 2 entries should remain intact.
|
||||
$count = $DB->count_records('glossary_entries', ['glossaryid' => $glossary2->id]);
|
||||
$this->assertEquals(1, $count);
|
||||
|
||||
$tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries',
|
||||
'itemid' => $ge2->id]);
|
||||
$this->assertEquals(2, $tagcount);
|
||||
|
||||
$aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge2->id]);
|
||||
$this->assertEquals(1, $aliascount);
|
||||
|
||||
$commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry',
|
||||
'itemid' => $ge2->id]);
|
||||
$this->assertEquals(1, $commentcount);
|
||||
|
||||
$ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry',
|
||||
'itemid' => $ge3->id]);
|
||||
$this->assertEquals(0, $ratingcount);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,8 +32,10 @@ use context_module;
|
||||
use stdClass;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
require_once($CFG->dirroot . '/mod/lesson/locallib.php');
|
||||
@@ -51,6 +53,7 @@ require_once($CFG->dirroot . '/mod/lesson/pagetypes/multichoice.php');
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\user_preference_provider {
|
||||
|
||||
@@ -166,6 +169,54 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'lesson' => 'lesson',
|
||||
'modulelevel' => CONTEXT_MODULE,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
// Mapping of lesson tables which may contain user data.
|
||||
$joins = [
|
||||
'lesson_attempts',
|
||||
'lesson_branch',
|
||||
'lesson_grades',
|
||||
'lesson_overrides',
|
||||
'lesson_timer',
|
||||
];
|
||||
|
||||
foreach ($joins as $join) {
|
||||
$sql = "
|
||||
SELECT lx.userid
|
||||
FROM {lesson} l
|
||||
JOIN {modules} m
|
||||
ON m.name = :lesson
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = l.id
|
||||
AND cm.module = m.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = cm.id
|
||||
AND ctx.contextlevel = :modulelevel
|
||||
JOIN {{$join}} lx
|
||||
ON lx.lessonid = l.id
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -445,6 +496,43 @@ class provider implements
|
||||
$DB->delete_records_select('lesson_overrides', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
$lessonid = static::get_lesson_id_from_context($context);
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
if (empty($lessonid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the SQL we'll need below.
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$sql = "lessonid = :lessonid AND userid {$insql}";
|
||||
$params = array_merge($inparams, ['lessonid' => $lessonid]);
|
||||
|
||||
// Delete the attempt files.
|
||||
$fs = get_file_storage();
|
||||
$recordset = $DB->get_recordset_select('lesson_attempts', $sql, $params, '', 'id, lessonid');
|
||||
foreach ($recordset as $record) {
|
||||
$fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $record->id);
|
||||
}
|
||||
$recordset->close();
|
||||
|
||||
// Delete all the things.
|
||||
$DB->delete_records_select('lesson_attempts', $sql, $params);
|
||||
$DB->delete_records_select('lesson_branch', $sql, $params);
|
||||
$DB->delete_records_select('lesson_grades', $sql, $params);
|
||||
$DB->delete_records_select('lesson_timer', $sql, $params);
|
||||
$DB->delete_records_select('lesson_overrides', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a survey ID from its context.
|
||||
*
|
||||
|
||||
@@ -108,6 +108,60 @@ class mod_lesson_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue(in_array($cm3ctx->id, $contextids));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for provider::get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$dg = $this->getDataGenerator();
|
||||
$c1 = $dg->create_course();
|
||||
$component = 'mod_lesson';
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$u3 = $dg->create_user();
|
||||
$u4 = $dg->create_user();
|
||||
$u5 = $dg->create_user();
|
||||
$u6 = $dg->create_user();
|
||||
|
||||
$cm1 = $dg->create_module('lesson', ['course' => $c1]);
|
||||
$cm2 = $dg->create_module('lesson', ['course' => $c1]);
|
||||
|
||||
$cm1ctx = context_module::instance($cm1->cmid);
|
||||
$cm2ctx = context_module::instance($cm2->cmid);
|
||||
|
||||
$this->create_attempt($cm1, $u1);
|
||||
$this->create_grade($cm1, $u2);
|
||||
$this->create_timer($cm1, $u3);
|
||||
$this->create_branch($cm1, $u4);
|
||||
$this->create_override($cm1, $u5);
|
||||
|
||||
$this->create_attempt($cm2, $u6);
|
||||
$this->create_grade($cm2, $u6);
|
||||
$this->create_timer($cm2, $u6);
|
||||
$this->create_branch($cm2, $u6);
|
||||
$this->create_override($cm2, $u6);
|
||||
|
||||
$context = context_module::instance($cm1->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
$this->assertCount(5, $userids);
|
||||
$expected = [$u1->id, $u2->id, $u3->id, $u4->id, $u5->id];
|
||||
$actual = $userids;
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$context = context_module::instance($cm2->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
$this->assertCount(1, $userids);
|
||||
$this->assertEquals([$u6->id], $userids);
|
||||
}
|
||||
|
||||
public function test_delete_data_for_all_users_in_context() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
@@ -293,6 +347,85 @@ class mod_lesson_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue($DB->record_exists('lesson_overrides', ['userid' => $u1->id, 'lessonid' => $cm2->id]));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
$c1 = $dg->create_course();
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
|
||||
$cm1 = $dg->create_module('lesson', ['course' => $c1]);
|
||||
$cm2 = $dg->create_module('lesson', ['course' => $c1]);
|
||||
$cm3 = $dg->create_module('lesson', ['course' => $c1]);
|
||||
$context1 = context_module::instance($cm1->cmid);
|
||||
$context3 = context_module::instance($cm3->cmid);
|
||||
|
||||
$this->create_attempt($cm1, $u1);
|
||||
$this->create_grade($cm1, $u1);
|
||||
$this->create_timer($cm1, $u1);
|
||||
$this->create_branch($cm1, $u1);
|
||||
$this->create_override($cm1, $u1);
|
||||
$this->create_attempt($cm1, $u2);
|
||||
$this->create_grade($cm1, $u2);
|
||||
$this->create_timer($cm1, $u2);
|
||||
$this->create_branch($cm1, $u2);
|
||||
$this->create_override($cm1, $u2);
|
||||
|
||||
$this->create_attempt($cm2, $u1);
|
||||
$this->create_grade($cm2, $u1);
|
||||
$this->create_timer($cm2, $u1);
|
||||
$this->create_branch($cm2, $u1);
|
||||
$this->create_override($cm2, $u1);
|
||||
$this->create_attempt($cm2, $u2);
|
||||
$this->create_grade($cm2, $u2);
|
||||
$this->create_timer($cm2, $u2);
|
||||
$this->create_branch($cm2, $u2);
|
||||
$this->create_override($cm2, $u2);
|
||||
|
||||
$assertnochange = function($user, $cm) use ($DB) {
|
||||
$this->assertTrue($DB->record_exists('lesson_attempts', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertTrue($DB->record_exists('lesson_grades', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertTrue($DB->record_exists('lesson_timer', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertTrue($DB->record_exists('lesson_branch', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertTrue($DB->record_exists('lesson_overrides', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
};
|
||||
|
||||
$assertdeleted = function($user, $cm) use ($DB) {
|
||||
$this->assertFalse($DB->record_exists('lesson_attempts', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertFalse($DB->record_exists('lesson_grades', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertFalse($DB->record_exists('lesson_timer', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertFalse($DB->record_exists('lesson_branch', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
$this->assertFalse($DB->record_exists('lesson_overrides', ['userid' => $user->id, 'lessonid' => $cm->id]));
|
||||
};
|
||||
|
||||
// Confirm existing state.
|
||||
$assertnochange($u1, $cm1);
|
||||
$assertnochange($u1, $cm2);
|
||||
$assertnochange($u2, $cm1);
|
||||
$assertnochange($u2, $cm2);
|
||||
|
||||
// Delete another module: no change.
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($context3, 'mod_lesson', [$u1->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
$assertnochange($u1, $cm1);
|
||||
$assertnochange($u1, $cm2);
|
||||
$assertnochange($u2, $cm1);
|
||||
$assertnochange($u2, $cm2);
|
||||
|
||||
// Delete cm1 for u1: no change for u2 and in cm2.
|
||||
$approveduserlist = new core_privacy\local\request\approved_userlist($context1, 'mod_lesson', [$u1->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
$assertdeleted($u1, $cm1);
|
||||
$assertnochange($u1, $cm2);
|
||||
$assertnochange($u2, $cm1);
|
||||
$assertnochange($u2, $cm2);
|
||||
}
|
||||
|
||||
public function test_export_data_for_user_overrides() {
|
||||
$dg = $this->getDataGenerator();
|
||||
$c1 = $dg->create_course();
|
||||
|
||||
@@ -25,9 +25,11 @@ namespace mod_lti\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -40,6 +42,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -157,6 +160,58 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all LTI submissions.
|
||||
$sql = "SELECT ltisub.userid
|
||||
FROM {context} c
|
||||
INNER JOIN {course_modules} cm
|
||||
ON cm.id = c.instanceid
|
||||
AND c.contextlevel = :contextlevel
|
||||
INNER JOIN {modules} m
|
||||
ON m.id = cm.module
|
||||
AND m.name = :modname
|
||||
INNER JOIN {lti} lti
|
||||
ON lti.id = cm.instance
|
||||
INNER JOIN {lti_submission} ltisub
|
||||
ON ltisub.ltiid = lti.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'modname' => 'lti',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
// Fetch all LTI types.
|
||||
$sql = "SELECT ltit.createdby AS userid
|
||||
FROM {context} c
|
||||
JOIN {course} course
|
||||
ON c.contextlevel = :contextlevel
|
||||
AND c.instanceid = course.id
|
||||
JOIN {lti_types} ltit
|
||||
ON ltit.course = course.id
|
||||
WHERE c.id = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
|
||||
*
|
||||
@@ -209,6 +264,27 @@ class provider implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_module) {
|
||||
$instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
|
||||
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
|
||||
$sql = "ltiid = :instanceid AND userid {$insql}";
|
||||
$params = array_merge(['instanceid' => $instanceid], $inparams);
|
||||
|
||||
$DB->delete_records_select('lti_submission', $sql, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export personal data for the given approved_contextlist related to LTI submissions.
|
||||
*
|
||||
|
||||
@@ -112,6 +112,47 @@ class mod_lti_privacy_provider_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->assertEquals(SYSCONTEXTID, $contextforsystem->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::test_get_users_in_context()
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$component = 'mod_lti';
|
||||
|
||||
// The LTI activity the user will have submitted something for.
|
||||
$lti1 = $this->getDataGenerator()->create_module('lti', array('course' => $course->id));
|
||||
|
||||
// Another LTI activity that has no user activity.
|
||||
$lti2 = $this->getDataGenerator()->create_module('lti', array('course' => $course->id));
|
||||
|
||||
// Create user which will make a submission each.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$this->create_lti_submission($lti1->id, $user1->id);
|
||||
$this->create_lti_submission($lti1->id, $user2->id);
|
||||
|
||||
$context = context_module::instance($lti1->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(2, $userlist);
|
||||
$expected = [$user1->id, $user2->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
$context = context_module::instance($lti2->cmid);
|
||||
$userlist = new \core_privacy\local\request\userlist($context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEmpty($userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::export_user_data().
|
||||
*/
|
||||
@@ -288,6 +329,51 @@ class mod_lti_privacy_provider_testcase extends \core_privacy\tests\provider_tes
|
||||
$this->assertEquals($user2->id, $lastsubmission->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$component = 'mod_lti';
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
$lti = $this->getDataGenerator()->create_module('lti', array('course' => $course->id));
|
||||
|
||||
// Create users that will make submissions.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$this->create_lti_submission($lti->id, $user1->id);
|
||||
$this->create_lti_submission($lti->id, $user2->id);
|
||||
$this->create_lti_submission($lti->id, $user3->id);
|
||||
|
||||
// Before deletion we should have 2 responses.
|
||||
$count = $DB->count_records('lti_submission', ['ltiid' => $lti->id]);
|
||||
$this->assertEquals(3, $count);
|
||||
|
||||
$context = \context_module::instance($lti->cmid);
|
||||
$approveduserids = [$user1->id, $user2->id];
|
||||
$approvedlist = new core_privacy\local\request\approved_userlist($context, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// After deletion the lti submission for the first two users should have been deleted.
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($approveduserids, SQL_PARAMS_NAMED);
|
||||
$sql = "ltiid = :ltiid AND userid {$insql}";
|
||||
$params = array_merge($inparams, ['ltiid' => $lti->id]);
|
||||
$count = $DB->count_records_select('lti_submission', $sql, $params);
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
// Check the submission for the third user is still there.
|
||||
$ltisubmission = $DB->get_records('lti_submission');
|
||||
$this->assertCount(1, $ltisubmission);
|
||||
$lastsubmission = reset($ltisubmission);
|
||||
$this->assertEquals($user3->id, $lastsubmission->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mimicks the creation of an LTI submission.
|
||||
*
|
||||
|
||||
@@ -28,9 +28,11 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
/**
|
||||
@@ -41,6 +43,7 @@ use core_privacy\local\request\writer;
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -103,6 +106,36 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sql = "SELECT ss.userid
|
||||
FROM {%s} ss
|
||||
JOIN {modules} m
|
||||
ON m.name = 'scorm'
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = ss.scormid
|
||||
AND cm.module = m.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = cm.id
|
||||
AND ctx.contextlevel = :modlevel
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
|
||||
|
||||
$userlist->add_from_sql('userid', sprintf($sql, 'scorm_scoes_track'), $params);
|
||||
$userlist->add_from_sql('userid', sprintf($sql, 'scorm_aicc_session'), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -290,6 +323,40 @@ class provider implements
|
||||
static::delete_data('scorm_aicc_session', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare SQL to gather all completed IDs.
|
||||
$userids = $userlist->get_userids();
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
|
||||
$sql = "SELECT ss.id
|
||||
FROM {%s} ss
|
||||
JOIN {modules} m
|
||||
ON m.name = 'scorm'
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = ss.scormid
|
||||
AND cm.module = m.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = cm.id
|
||||
WHERE ctx.id = :contextid
|
||||
AND ss.userid $insql";
|
||||
$params = array_merge($inparams, ['contextid' => $context->id]);
|
||||
|
||||
static::delete_data('scorm_scoes_track', $sql, $params);
|
||||
static::delete_data('scorm_aicc_session', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete data from $tablename with the IDs returned by $sql query.
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use mod_scorm\privacy\provider;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
|
||||
@@ -68,6 +69,28 @@ class mod_scorm_testcase extends provider_testcase {
|
||||
$this->assertContains($this->context->id, $contextlist->get_contextids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting the user IDs for the context related to this plugin.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
$this->scorm_setup_test_scenario_data();
|
||||
$component = 'mod_scorm';
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($this->context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
// Students 1 and 2 have attempts in the SCORM context, student 0 does not.
|
||||
$this->assertCount(2, $userlist);
|
||||
|
||||
$expected = [$this->student1->id, $this->student2->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data is exported correctly for this plugin.
|
||||
*/
|
||||
@@ -196,9 +219,59 @@ class mod_scorm_testcase extends provider_testcase {
|
||||
$this->assertEquals(2, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$component = 'mod_scorm';
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
$this->scorm_setup_test_scenario_data();
|
||||
|
||||
// Before deletion, we should have 8 entries in the scorm_scoes_track table.
|
||||
$count = $DB->count_records('scorm_scoes_track');
|
||||
$this->assertEquals(8, $count);
|
||||
// Before deletion, we should have 4 entries in the scorm_aicc_session table.
|
||||
$count = $DB->count_records('scorm_aicc_session');
|
||||
$this->assertEquals(4, $count);
|
||||
|
||||
// Delete only student 1's data, retain student 2's data.
|
||||
$approveduserids = [$this->student1->id];
|
||||
$approvedlist = new approved_userlist($this->context, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
// After deletion, the scorm_scoes_track entries for the first student should have been deleted.
|
||||
$count = $DB->count_records('scorm_scoes_track', ['userid' => $this->student1->id]);
|
||||
$this->assertEquals(0, $count);
|
||||
$count = $DB->count_records('scorm_scoes_track');
|
||||
$this->assertEquals(4, $count);
|
||||
|
||||
// After deletion, the scorm_aicc_session entries for the first student should have been deleted.
|
||||
$count = $DB->count_records('scorm_aicc_session', ['userid' => $this->student1->id]);
|
||||
$this->assertEquals(0, $count);
|
||||
$count = $DB->count_records('scorm_aicc_session');
|
||||
$this->assertEquals(2, $count);
|
||||
|
||||
// Confirm that the SCORM hasn't been removed.
|
||||
$scormcount = $DB->get_records('scorm');
|
||||
$this->assertCount(1, (array) $scormcount);
|
||||
|
||||
// Delete scoes_track for student0 (nothing has to be removed).
|
||||
$approveduserids = [$this->student0->id];
|
||||
$approvedlist = new approved_userlist($this->context, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
$count = $DB->count_records('scorm_scoes_track');
|
||||
$this->assertEquals(4, $count);
|
||||
$count = $DB->count_records('scorm_aicc_session');
|
||||
$this->assertEquals(2, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to setup 3 users and 2 SCORM attempts for student1 and student2.
|
||||
* $this->student0 is always created withot any attempt.
|
||||
* $this->student0 is always created without any attempt.
|
||||
*/
|
||||
protected function scorm_setup_test_scenario_data() {
|
||||
global $DB;
|
||||
|
||||
@@ -31,8 +31,10 @@ use context_helper;
|
||||
use context_module;
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
require_once($CFG->dirroot . '/mod/survey/lib.php');
|
||||
@@ -47,6 +49,7 @@ require_once($CFG->dirroot . '/mod/survey/lib.php');
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -113,6 +116,61 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'survey' => 'survey',
|
||||
'modulelevel' => CONTEXT_MODULE,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
$sql = "
|
||||
SELECT sa.userid
|
||||
FROM {survey} s
|
||||
JOIN {modules} m
|
||||
ON m.name = :survey
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = s.id
|
||||
AND cm.module = m.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = cm.id
|
||||
AND ctx.contextlevel = :modulelevel
|
||||
JOIN {survey_answers} sa
|
||||
ON sa.survey = s.id
|
||||
WHERE ctx.id = :contextid
|
||||
AND s.template <> 0";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT sy.userid
|
||||
FROM {survey} s
|
||||
JOIN {modules} m
|
||||
ON m.name = :survey
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = s.id
|
||||
AND cm.module = m.id
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = cm.id
|
||||
AND ctx.contextlevel = :modulelevel
|
||||
JOIN {survey_analysis} sy
|
||||
ON sy.survey = s.id
|
||||
WHERE ctx.id = :contextid
|
||||
AND s.template <> 0";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -275,6 +333,44 @@ class provider implements
|
||||
$DB->delete_records_select('survey_analysis', "survey $insql AND userid = :userid", $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context->contextlevel != CONTEXT_MODULE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the survey ID.
|
||||
$sql = "
|
||||
SELECT s.id
|
||||
FROM {survey} s
|
||||
JOIN {modules} m
|
||||
ON m.name = :survey
|
||||
JOIN {course_modules} cm
|
||||
ON cm.instance = s.id
|
||||
AND cm.module = m.id
|
||||
WHERE cm.id = :cmid";
|
||||
$params = [
|
||||
'survey' => 'survey',
|
||||
'cmid' => $context->instanceid,
|
||||
];
|
||||
$surveyid = $DB->get_field_sql($sql, $params);
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
// Delete all the things.
|
||||
list($insql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$params['surveyid'] = $surveyid;
|
||||
|
||||
$DB->delete_records_select('survey_answers', "survey = :surveyid AND userid {$insql}", $params);
|
||||
$DB->delete_records_select('survey_analysis', "survey = :surveyid AND userid {$insql}", $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a survey ID from its context.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,7 @@ global $CFG;
|
||||
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\writer;
|
||||
use mod_survey\privacy\provider;
|
||||
@@ -85,6 +86,59 @@ class mod_survey_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue(in_array(context_module::instance($cm1c->cmid)->id, $contextids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::test_get_users_in_context().
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$dg = $this->getDataGenerator();
|
||||
$component = 'mod_survey';
|
||||
|
||||
$c1 = $dg->create_course();
|
||||
$c2 = $dg->create_course();
|
||||
$cm1a = $dg->create_module('survey', ['template' => 1, 'course' => $c1]);
|
||||
$cm1b = $dg->create_module('survey', ['template' => 2, 'course' => $c1]);
|
||||
$cm2 = $dg->create_module('survey', ['template' => 1, 'course' => $c2]);
|
||||
$cm1acontext = context_module::instance($cm1a->cmid);
|
||||
$cm1bcontext = context_module::instance($cm1b->cmid);
|
||||
$cm2context = context_module::instance($cm2->cmid);
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
$bothusers = [$u1->id, $u2->id];
|
||||
sort($bothusers);
|
||||
|
||||
$this->create_answer($cm1a->id, 1, $u1->id);
|
||||
$this->create_answer($cm1b->id, 1, $u1->id);
|
||||
$this->create_answer($cm1b->id, 1, $u2->id);
|
||||
$this->create_answer($cm2->id, 1, $u2->id);
|
||||
$this->create_analysis($cm2->id, $u1->id);
|
||||
|
||||
// Cm1a should only contain u1.
|
||||
$userlist = new \core_privacy\local\request\userlist($cm1acontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(1, $userlist);
|
||||
$this->assertEquals([$u1->id], $userlist->get_userids());
|
||||
|
||||
// Cm1b should contain u1 and u2 (both have answers).
|
||||
$userlist = new \core_privacy\local\request\userlist($cm1bcontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(2, $userlist);
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
$this->assertEquals($bothusers, $actual);
|
||||
|
||||
// Cm2 should contain u1 (analysis) and u2 (answer).
|
||||
$userlist = new \core_privacy\local\request\userlist($cm2context, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(2, $userlist);
|
||||
$actual = $userlist->get_userids();
|
||||
sort($actual);
|
||||
$this->assertEquals($bothusers, $actual);
|
||||
}
|
||||
|
||||
public function test_delete_data_for_all_users_in_context() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
@@ -190,6 +244,64 @@ class mod_survey_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for provider::delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
$component = 'mod_survey';
|
||||
|
||||
$c1 = $dg->create_course();
|
||||
$cm1a = $dg->create_module('survey', ['template' => 1, 'course' => $c1]);
|
||||
$cm1b = $dg->create_module('survey', ['template' => 2, 'course' => $c1]);
|
||||
$cm1c = $dg->create_module('survey', ['template' => 2, 'course' => $c1]);
|
||||
$cm1acontext = context_module::instance($cm1a->cmid);
|
||||
$cm1bcontext = context_module::instance($cm1b->cmid);
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
$u2 = $dg->create_user();
|
||||
|
||||
$this->create_answer($cm1a->id, 1, $u1->id);
|
||||
$this->create_answer($cm1a->id, 1, $u2->id);
|
||||
$this->create_analysis($cm1a->id, $u1->id);
|
||||
$this->create_analysis($cm1a->id, $u2->id);
|
||||
$this->create_answer($cm1b->id, 1, $u2->id);
|
||||
$this->create_analysis($cm1b->id, $u1->id);
|
||||
$this->create_answer($cm1c->id, 1, $u1->id);
|
||||
$this->create_analysis($cm1c->id, $u2->id);
|
||||
|
||||
// Confirm data exists before deletion.
|
||||
$this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id]));
|
||||
|
||||
// Ensure only approved user data is deleted.
|
||||
$approveduserids = [$u1->id];
|
||||
$approvedlist = new approved_userlist($cm1acontext, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
$this->assertFalse($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id]));
|
||||
$this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id]));
|
||||
|
||||
$approveduserids = [$u1->id, $u2->id];
|
||||
$approvedlist = new approved_userlist($cm1bcontext, $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
$this->assertFalse($DB->record_exists('survey_answers', ['survey' => $cm1b->id]));
|
||||
$this->assertFalse($DB->record_exists('survey_analysis', ['survey' => $cm1b->id]));
|
||||
|
||||
$this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id]));
|
||||
$this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id]));
|
||||
}
|
||||
|
||||
public function test_export_data_for_user() {
|
||||
global $DB;
|
||||
$dg = $this->getDataGenerator();
|
||||
|
||||
@@ -26,11 +26,13 @@ namespace mod_wiki\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use context_user;
|
||||
use context;
|
||||
use core_privacy\local\request\helper;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -44,6 +46,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
\core_privacy\local\request\plugin\provider {
|
||||
|
||||
/**
|
||||
@@ -119,6 +122,81 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users who have data within a context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!is_a($context, \context_module::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'modname' => 'wiki',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
$sql = "
|
||||
SELECT s.userid
|
||||
FROM {modules} m
|
||||
JOIN {course_modules} cm ON cm.module = m.id AND m.name = :modname
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
JOIN {wiki_subwikis} s ON cm.instance = s.wikiid
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT p.userid
|
||||
FROM {modules} m
|
||||
JOIN {course_modules} cm ON cm.module = m.id AND m.name = :modname
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
JOIN {wiki_subwikis} s ON cm.instance = s.wikiid
|
||||
JOIN {wiki_pages} p ON p.subwikiid = s.id
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT v.userid
|
||||
FROM {modules} m
|
||||
JOIN {course_modules} cm ON cm.module = m.id AND m.name = :modname
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
JOIN {wiki_subwikis} s ON cm.instance = s.wikiid
|
||||
JOIN {wiki_pages} p ON p.subwikiid = s.id
|
||||
JOIN {wiki_versions} v ON v.pageid = p.id
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT l.userid
|
||||
FROM {modules} m
|
||||
JOIN {course_modules} cm ON cm.module = m.id AND m.name = :modname
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
JOIN {wiki_subwikis} s ON cm.instance = s.wikiid
|
||||
JOIN {wiki_pages} p ON p.subwikiid = s.id
|
||||
JOIN {wiki_locks} l ON l.pageid = p.id
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT p.id
|
||||
FROM {modules} m
|
||||
JOIN {course_modules} cm ON cm.module = m.id AND m.name = :modname
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
|
||||
JOIN {wiki_subwikis} s ON cm.instance = s.wikiid
|
||||
JOIN {wiki_pages} p ON p.subwikiid = s.id
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
\core_comment\privacy\provider::get_users_in_context_from_sql($userlist, 'com', 'mod_wiki', 'wiki_page', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one subwiki to the export
|
||||
*
|
||||
@@ -491,4 +569,74 @@ class provider implements
|
||||
// Remove comments made by this user on all other wiki pages.
|
||||
\core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_wiki', 'wiki_page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
$context = $userlist->get_context();
|
||||
$userids = $userlist->get_userids();
|
||||
|
||||
if ($context->contextlevel != CONTEXT_MODULE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove only individual subwikis. Contributions to collaborative wikis is not considered personal contents.
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$params = [
|
||||
'wiki' => 'wiki',
|
||||
'contextmod' => CONTEXT_MODULE,
|
||||
'contextid' => $context->id,
|
||||
];
|
||||
|
||||
$params = array_merge($inparams, $params);
|
||||
$sql = "SELECT s.id
|
||||
FROM {context} ctx
|
||||
JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmod
|
||||
JOIN {modules} m ON m.name = :wiki AND cm.module = m.id
|
||||
JOIN {wiki_subwikis} s ON s.wikiid = cm.instance
|
||||
WHERE ctx.id = :contextid
|
||||
AND s.userid {$insql}";
|
||||
|
||||
$subwikis = $DB->get_fieldset_sql($sql, $params);
|
||||
|
||||
if ($subwikis) {
|
||||
// We found individual subwikis that need to be deleted completely.
|
||||
|
||||
$fs = get_file_storage();
|
||||
foreach ($subwikis as $subwikiid) {
|
||||
$fs->delete_area_files($context->id, 'mod_wiki', 'attachments', $subwikiid);
|
||||
\core_comment\privacy\provider::delete_comments_for_all_users_select(context::instance_by_id($context->id),
|
||||
'mod_wiki', 'wiki_page', "IN (SELECT id FROM {wiki_pages} WHERE subwikiid=:subwikiid)",
|
||||
['subwikiid' => $subwikiid]);
|
||||
}
|
||||
|
||||
list($insql, $inparams) = $DB->get_in_or_equal($subwikis, SQL_PARAMS_NAMED);
|
||||
$params = ['component' => 'mod_wiki', 'itemtype' => 'page'];
|
||||
$params = array_merge($inparams, $params);
|
||||
$sql = "DELETE FROM {tag_instance}
|
||||
WHERE component=:component
|
||||
AND itemtype=:itemtype
|
||||
AND itemid IN
|
||||
(SELECT id
|
||||
FROM {wiki_pages}
|
||||
WHERE subwikiid $insql)";
|
||||
|
||||
$DB->execute($sql, $params);
|
||||
|
||||
$DB->delete_records_select('wiki_locks', "pageid IN (SELECT id FROM {wiki_pages} WHERE subwikiid {$insql})", $params);
|
||||
$DB->delete_records_select('wiki_versions', "pageid IN (SELECT id FROM {wiki_pages} WHERE subwikiid {$insql})",
|
||||
$params);
|
||||
$DB->delete_records_select('wiki_synonyms', "subwikiid {$insql}", $params);
|
||||
$DB->delete_records_select('wiki_links', "subwikiid {$insql}", $params);
|
||||
$DB->delete_records_select('wiki_pages', "subwikiid {$insql}", $params);
|
||||
$DB->delete_records_select('wiki_subwikis', "id {$insql}", $params);
|
||||
}
|
||||
|
||||
// Remove comments made by this user on all other wiki pages.
|
||||
\core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_wiki', 'wiki_page');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ global $CFG;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use mod_wiki\privacy\provider;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
require_once($CFG->dirroot.'/mod/wiki/locallib.php');
|
||||
@@ -80,11 +81,13 @@ class mod_wiki_privacy_testcase extends provider_testcase {
|
||||
$this->users[1] = $dg->create_user();
|
||||
$this->users[2] = $dg->create_user();
|
||||
$this->users[3] = $dg->create_user();
|
||||
$this->users[4] = $dg->create_user();
|
||||
|
||||
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
|
||||
$this->getDataGenerator()->enrol_user($this->users[1]->id, $course->id, $studentrole->id, 'manual');
|
||||
$this->getDataGenerator()->enrol_user($this->users[2]->id, $course->id, $studentrole->id, 'manual');
|
||||
$this->getDataGenerator()->enrol_user($this->users[3]->id, $course->id, $studentrole->id, 'manual');
|
||||
$this->getDataGenerator()->enrol_user($this->users[4]->id, $course->id, $studentrole->id, 'manual');
|
||||
|
||||
$cm1 = $this->getDataGenerator()->create_module('wiki', ['course' => $course->id]);
|
||||
$cm2 = $this->getDataGenerator()->create_module('wiki', ['course' => $course->id, 'wikimode' => 'individual']);
|
||||
@@ -133,6 +136,10 @@ class mod_wiki_privacy_testcase extends provider_testcase {
|
||||
// Lock a page in the third wiki without having any revisions on it.
|
||||
wiki_set_lock($this->pages[3][1]->id, $this->users[3]->id, null, true);
|
||||
|
||||
// User 4 - added to the first wiki, so all users are not part of all edited contexts.
|
||||
$this->setUser($this->users[4]);
|
||||
$this->pages[1][4] = $this->create_page($cm1);
|
||||
|
||||
$this->subwikis = [
|
||||
1 => $this->pages[1][1]->subwikiid,
|
||||
21 => $this->pages[21][1]->subwikiid,
|
||||
@@ -153,6 +160,7 @@ class mod_wiki_privacy_testcase extends provider_testcase {
|
||||
1 => $this->pages[1][1]->id . ' ' . $this->pages[1][1]->title,
|
||||
2 => $this->pages[1][2]->id . ' ' . $this->pages[1][2]->title,
|
||||
3 => $this->pages[1][3]->id . ' ' . $this->pages[1][3]->title,
|
||||
4 => $this->pages[1][4]->id . ' ' . $this->pages[1][4]->title,
|
||||
],
|
||||
21 => [
|
||||
1 => $this->pages[21][1]->id . ' ' . $this->pages[21][1]->title,
|
||||
@@ -261,6 +269,60 @@ class mod_wiki_privacy_testcase extends provider_testcase {
|
||||
], $contextids, '', 0.0, 10, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting the users within a context.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
global $DB;
|
||||
$component = 'mod_wiki';
|
||||
|
||||
// Add a comment from user 4 in context 3.
|
||||
$this->setUser($this->users[4]);
|
||||
$this->add_comment($this->pages[3][1], 'Look at me, getting involved!');
|
||||
|
||||
// Ensure userlist for context 1 contains all users.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->contexts[1], $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(4, $userlist);
|
||||
|
||||
$expected = [$this->users[1]->id, $this->users[2]->id, $this->users[3]->id, $this->users[4]->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Ensure userlist for context 2 contains users 1-3 only.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->contexts[2], $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(3, $userlist);
|
||||
|
||||
$expected = [$this->users[1]->id, $this->users[2]->id, $this->users[3]->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Ensure userlist for context 3 contains users 2, 3 and 4 only.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->contexts[3], $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertCount(3, $userlist);
|
||||
|
||||
$expected = [$this->users[2]->id, $this->users[3]->id, $this->users[4]->id];
|
||||
$actual = $userlist->get_userids();
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Ensure userlist for context 4 is empty.
|
||||
$userlist = new \core_privacy\local\request\userlist($this->contexts[4], $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEmpty($userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export data for user 1
|
||||
*/
|
||||
@@ -529,4 +591,63 @@ class mod_wiki_privacy_testcase extends provider_testcase {
|
||||
$this->assertTrue(writer::with_context($this->contexts[1])->has_any_data());
|
||||
$this->assertFalse(writer::with_context($this->contexts[2])->has_any_data());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for delete_data_for_users().
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$component = 'mod_wiki';
|
||||
|
||||
// Ensure data exists within context 2 - individual wikis.
|
||||
// Since each user owns their own subwiki in this context, they can be deleted.
|
||||
$u1ctx2 = new approved_contextlist($this->users[1], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u1ctx2);
|
||||
$u2ctx2 = new approved_contextlist($this->users[2], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u2ctx2);
|
||||
$u3ctx2 = new approved_contextlist($this->users[3], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u3ctx2);
|
||||
|
||||
$this->assertTrue(writer::with_context($this->contexts[2])->has_any_data());
|
||||
writer::reset();
|
||||
|
||||
// Delete user 1 and 2 data, user 3's wiki still remains.
|
||||
$approveduserids = [$this->users[1]->id, $this->users[2]->id];
|
||||
$approvedlist = new approved_userlist($this->contexts[2], $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
$u1ctx2 = new approved_contextlist($this->users[1], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u1ctx2);
|
||||
$u2ctx2 = new approved_contextlist($this->users[2], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u2ctx2);
|
||||
$u3ctx2 = new approved_contextlist($this->users[3], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u3ctx2);
|
||||
|
||||
$this->assertTrue(writer::with_context($this->contexts[2])->has_any_data());
|
||||
writer::reset();
|
||||
|
||||
// Delete user 3's wiki. All 3 subwikis now deleted, so ensure no data is found in this context.
|
||||
$approveduserids = [$this->users[3]->id];
|
||||
$approvedlist = new approved_userlist($this->contexts[2], $component, $approveduserids);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
|
||||
$u1ctx2 = new approved_contextlist($this->users[1], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u1ctx2);
|
||||
$u2ctx2 = new approved_contextlist($this->users[2], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u2ctx2);
|
||||
$u3ctx2 = new approved_contextlist($this->users[3], 'mod_wiki', [$this->contexts[2]->id]);
|
||||
provider::export_user_data($u3ctx2);
|
||||
|
||||
$this->assertFalse(writer::with_context($this->contexts[2])->has_any_data());
|
||||
writer::reset();
|
||||
|
||||
// Ensure Context 1 still contains data.
|
||||
$u1ctx1 = new approved_contextlist($this->users[1], 'mod_wiki', [$this->contexts[1]->id]);
|
||||
provider::export_user_data($u1ctx1);
|
||||
$u2ctx1 = new approved_contextlist($this->users[2], 'mod_wiki', [$this->contexts[1]->id]);
|
||||
provider::export_user_data($u2ctx1);
|
||||
$u3ctx1 = new approved_contextlist($this->users[3], 'mod_wiki', [$this->contexts[1]->id]);
|
||||
provider::export_user_data($u3ctx1);
|
||||
|
||||
$this->assertTrue(writer::with_context($this->contexts[1])->has_any_data());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Interface for deleting users related to a context.
|
||||
*
|
||||
* @package core_plagiarism
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_plagiarism\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Interface for the plagiarism system.
|
||||
*
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface plagiarism_user_provider extends
|
||||
// The plagiarism_provider should be implemented by plugins which only provide information to a subsystem.
|
||||
\core_privacy\local\request\plugin\subsystem_provider {
|
||||
|
||||
/**
|
||||
* Delete all user information for the provided users and context.
|
||||
*
|
||||
* @param array $userids The users to delete
|
||||
* @param \context $context The context to refine the deletion.
|
||||
*/
|
||||
public static function delete_plagiarism_for_users(array $userids, \context $context);
|
||||
}
|
||||
@@ -85,6 +85,16 @@ class provider implements
|
||||
static::call_plugin_method('delete_plagiarism_for_user', [$userid, $context]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all user content for a user in a context in all plagiarism plugins.
|
||||
*
|
||||
* @param array $userids The users to delete
|
||||
* @param \context $context The context to refine the deletion.
|
||||
*/
|
||||
public static function delete_plagiarism_for_users(array $userids, \context $context) {
|
||||
static::call_plugin_method('delete_plagiarism_for_users', [$userids, $context]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for looping through all of the plagiarism plugins and calling a method.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,8 @@ use core_privacy\local\request\context;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Provider for the portfolio API.
|
||||
@@ -41,6 +43,7 @@ class provider implements
|
||||
// The core portfolio system stores preferences related to the other portfolio subsystems.
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\plugin\provider,
|
||||
\core_privacy\local\request\core_userlist_provider,
|
||||
// The portfolio subsystem will be called by other components.
|
||||
\core_privacy\local\request\subsystem\plugin_provider {
|
||||
|
||||
@@ -96,6 +99,42 @@ class provider implements
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of users within a specific context.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if (!$context instanceof \context_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id,
|
||||
'contextuser' => CONTEXT_USER,
|
||||
];
|
||||
|
||||
$sql = "SELECT ctx.instanceid as userid
|
||||
FROM {portfolio_instance_user} piu
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = piu.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
|
||||
$sql = "SELECT ctx.instanceid as userid
|
||||
FROM {portfolio_log} pl
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = pl.userid
|
||||
AND ctx.contextlevel = :contextuser
|
||||
WHERE ctx.id = :contextid";
|
||||
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
@@ -191,6 +230,23 @@ class provider implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
global $DB;
|
||||
|
||||
$context = $userlist->get_context();
|
||||
|
||||
if ($context instanceof \context_user) {
|
||||
$DB->delete_records('portfolio_instance_user', ['userid' => $context->instanceid]);
|
||||
$DB->delete_records('portfolio_tempdata', ['userid' => $context->instanceid]);
|
||||
$DB->delete_records('portfolio_log', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_portfolio\privacy\provider;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Privacy provider tests class.
|
||||
*
|
||||
@@ -70,7 +73,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc
|
||||
*/
|
||||
public function test_get_metadata() {
|
||||
$collection = new \core_privacy\local\metadata\collection('core_portfolio');
|
||||
$collection = \core_portfolio\privacy\provider::get_metadata($collection);
|
||||
$collection = provider::get_metadata($collection);
|
||||
$this->assertNotEmpty($collection);
|
||||
$items = $collection->get_collection();
|
||||
$this->assertEquals(4, count($items));
|
||||
@@ -88,7 +91,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
$this->create_portfolio_data('googledocs', 'Google Docs', $user, 'visible', 1);
|
||||
$contextlist = \core_portfolio\privacy\provider::get_contexts_for_userid($user->id);
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
$this->assertEquals($context->id, $contextlist->current()->id);
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc
|
||||
$context = context_user::instance($user->id);
|
||||
$this->create_portfolio_data('googledocs', 'Google Docs', $user, 'visible', 1);
|
||||
$contextlist = new \core_privacy\local\request\approved_contextlist($user, 'core_portfolio', [$context->id]);
|
||||
\core_portfolio\privacy\provider::export_user_data($contextlist);
|
||||
provider::export_user_data($contextlist);
|
||||
$writer = \core_privacy\local\request\writer::with_context($context);
|
||||
$portfoliodata = $writer->get_data([get_string('privacy:path', 'portfolio')]);
|
||||
$this->assertEquals('Google Docs', $portfoliodata->{'Google Docs'}->name);
|
||||
@@ -120,12 +123,12 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc
|
||||
$this->create_portfolio_data('onedrive', 'Microsoft onedrive', $user2, 'visible', 1);
|
||||
// Check a system context sent through.
|
||||
$systemcontext = context_system::instance();
|
||||
\core_portfolio\privacy\provider::delete_data_for_all_users_in_context($systemcontext);
|
||||
provider::delete_data_for_all_users_in_context($systemcontext);
|
||||
$records = $DB->get_records('portfolio_instance_user');
|
||||
$this->assertCount(2, $records);
|
||||
$this->assertCount(4, $DB->get_records('portfolio_log'));
|
||||
$context = context_user::instance($user1->id);
|
||||
\core_portfolio\privacy\provider::delete_data_for_all_users_in_context($context);
|
||||
provider::delete_data_for_all_users_in_context($context);
|
||||
$records = $DB->get_records('portfolio_instance_user');
|
||||
// Only one entry should remain for user 2.
|
||||
$this->assertCount(1, $records);
|
||||
@@ -152,7 +155,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc
|
||||
|
||||
$context = context_user::instance($user1->id);
|
||||
$contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_portfolio', [$context->id]);
|
||||
\core_portfolio\privacy\provider::delete_data_for_user($contextlist);
|
||||
provider::delete_data_for_user($contextlist);
|
||||
$records = $DB->get_records('portfolio_instance_user');
|
||||
// Only one entry should remain for user 2.
|
||||
$this->assertCount(1, $records);
|
||||
@@ -160,4 +163,97 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc
|
||||
$this->assertEquals($user2->id, $data->userid);
|
||||
$this->assertCount(2, $DB->get_records('portfolio_log'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only users with a user context are fetched.
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'core_portfolio';
|
||||
// Create a user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$usercontext = context_user::instance($user->id);
|
||||
// The list of users should not return anything yet (related data still haven't been created).
|
||||
$userlist = new \core_privacy\local\request\userlist($usercontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
|
||||
// Create portfolio data for user.
|
||||
$this->create_portfolio_data('googledocs', 'Google Docs', $user,
|
||||
'visible', 1);
|
||||
|
||||
// The list of users for user context should return the user.
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(1, $userlist);
|
||||
$expected = [$user->id];
|
||||
$actual = $userlist->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// The list of users for system context should not return any users.
|
||||
$systemcontext = context_system::instance();
|
||||
$userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
|
||||
provider::get_users_in_context($userlist);
|
||||
$this->assertCount(0, $userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that data for users in approved userlist is deleted.
|
||||
*/
|
||||
public function test_delete_data_for_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$component = 'core_portfolio';
|
||||
// Create user1.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$usercontext1 = context_user::instance($user1->id);
|
||||
// Create user1.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$usercontext2 = context_user::instance($user2->id);
|
||||
|
||||
// Create portfolio data for user1 and user2.
|
||||
$this->create_portfolio_data('googledocs', 'Google Docs', $user1,
|
||||
'visible', 1);
|
||||
$this->create_portfolio_data('onedrive', 'Microsoft onedrive', $user2,
|
||||
'visible', 1);
|
||||
|
||||
// The list of users for usercontext1 should return user1.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
$expected = [$user1->id];
|
||||
$actual = $userlist1->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
// The list of users for usercontext2 should return user2.
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$expected = [$user2->id];
|
||||
$actual = $userlist2->get_userids();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Add userlist1 to the approved user list.
|
||||
$approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids());
|
||||
// Delete user data using delete_data_for_user for usercontext1.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
// Re-fetch users in usercontext1 - The user list should now be empty.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(0, $userlist1);
|
||||
// Re-fetch users in usercontext2 - The user list should not be empty (user2).
|
||||
$userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
|
||||
// User data should be only removed in the user context.
|
||||
$systemcontext = context_system::instance();
|
||||
// Add userlist2 to the approved user list in the system context.
|
||||
$approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids());
|
||||
// Delete user1 data using delete_data_for_user.
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
// Re-fetch users in usercontext2 - The user list should not be empty (user2).
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext2, $component);
|
||||
provider::get_users_in_context($userlist1);
|
||||
$this->assertCount(1, $userlist1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* An implementation of a userlist which has been filtered and approved.
|
||||
*
|
||||
* @package core_privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_privacy\local\request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* An implementation of a userlist which has been filtered and approved.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class approved_userlist extends userlist_base {
|
||||
|
||||
/**
|
||||
* Create a new approved userlist.
|
||||
*
|
||||
* @param \context $context The context.
|
||||
* @param string $component the frankenstyle component name.
|
||||
* @param \int[] $userids The list of userids present in this list.
|
||||
*/
|
||||
public function __construct(\context $context, string $component, array $userids) {
|
||||
parent::__construct($context, $component);
|
||||
|
||||
$this->set_userids($userids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an approved userlist from a userlist.
|
||||
*
|
||||
* @param userlist $userlist The source list
|
||||
* @return approved_userlist The newly created approved userlist.
|
||||
*/
|
||||
public static function create_from_userlist(userlist $userlist) : approved_userlist {
|
||||
$newlist = new static($userlist->get_context(), $userlist->get_component(), $userlist->get_userids());
|
||||
|
||||
return $newlist;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This file contains an interface to describe classes which provide user data in some form to core.
|
||||
*
|
||||
* @package core_privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_privacy\local\request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* The interface is used to describe a provider which is capable of identifying the users who have data within it.
|
||||
*
|
||||
* It describes data how these requests are serviced in a specific format.
|
||||
*
|
||||
* @package core_privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
*/
|
||||
interface core_userlist_provider {
|
||||
|
||||
/**
|
||||
* Get the list of contexts that contain user information for the specified user.
|
||||
*
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist);
|
||||
|
||||
/**
|
||||
* Delete multiple users within a single context.
|
||||
*
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist);
|
||||
}
|
||||
@@ -60,6 +60,18 @@ class helper {
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add core-controlled contexts which are related to a component but that component may know about.
|
||||
*
|
||||
* For example, most activities are not aware of activity completion, but the course implements it for them.
|
||||
* These should be included.
|
||||
*
|
||||
* @param \core_privacy\local\request\userlist $userlist
|
||||
* @return contextlist The final contextlist
|
||||
*/
|
||||
public static function add_shared_users_to_userlist(\core_privacy\local\request\userlist $userlist) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle export of standard data for a plugin which implements the null provider and does not normally store data
|
||||
* of its own.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* List of users from the Privacy API Search functions.
|
||||
*
|
||||
* @package core_privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_privacy\local\request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* List of users from the Privacy API Search functions.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class userlist extends userlist_base {
|
||||
|
||||
/**
|
||||
* Add a set of users from SQL.
|
||||
*
|
||||
* The SQL should only return a list of user IDs.
|
||||
*
|
||||
* @param string $fieldname The name of the field which holds the user id
|
||||
* @param string $sql The SQL which will fetch the list of * user IDs
|
||||
* @param array $params The set of SQL parameters
|
||||
* @return $this
|
||||
*/
|
||||
public function add_from_sql(string $fieldname, string $sql, array $params) : userlist {
|
||||
global $DB;
|
||||
|
||||
// Able to guess a field name.
|
||||
$wrapper = "
|
||||
SELECT DISTINCT u.id
|
||||
FROM {user} u
|
||||
JOIN ({$sql}) target ON u.id = target.{$fieldname}";
|
||||
|
||||
$users = $DB->get_records_sql($wrapper, $params);
|
||||
$this->add_userids(array_keys($users));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the user user for a given user.
|
||||
*
|
||||
* @param int $userid
|
||||
* @return $this
|
||||
*/
|
||||
public function add_user(int $userid) : userlist {
|
||||
$this->add_users([$userid]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the user users for given users.
|
||||
*
|
||||
* @param int[] $userids
|
||||
* @return $this
|
||||
*/
|
||||
public function add_users(array $userids) : userlist {
|
||||
global $DB;
|
||||
|
||||
list($useridsql, $useridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
|
||||
$sql = "SELECT DISTINCT u.id
|
||||
FROM {user} u
|
||||
WHERE u.id {$useridsql}";
|
||||
$this->add_from_sql('id', $sql, $useridparams);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the component for this userlist.
|
||||
*
|
||||
* @param string $component the frankenstyle component name.
|
||||
* @return $this
|
||||
*/
|
||||
public function set_component($component) : userlist_base {
|
||||
parent::set_component($component);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Base implementation of a userlist.
|
||||
*
|
||||
* @package core_privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_privacy\local\request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Base implementation of a userlist used to store a set of users.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class userlist_base implements
|
||||
// Implement an Iterator to fetch the Context objects.
|
||||
\Iterator,
|
||||
|
||||
// Implement the Countable interface to allow the number of returned results to be queried easily.
|
||||
\Countable {
|
||||
|
||||
/**
|
||||
* @var array List of user IDs.
|
||||
*
|
||||
* Note: this must not be updated using set_userids only as this
|
||||
* ensures uniqueness.
|
||||
*/
|
||||
private $userids = [];
|
||||
|
||||
/**
|
||||
* @var string component the frankenstyle component name.
|
||||
*/
|
||||
protected $component = '';
|
||||
|
||||
/**
|
||||
* @var int Current position of the iterator.
|
||||
*/
|
||||
protected $iteratorposition = 0;
|
||||
|
||||
/** @var \context The context that this userlist belongs to */
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* Constructor to create a new userlist.
|
||||
*
|
||||
* @param \context $context
|
||||
* @param string $component
|
||||
*/
|
||||
public function __construct(\context $context, string $component) {
|
||||
$this->context = $context;
|
||||
$this->set_component($component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the userids.
|
||||
*
|
||||
* @param array $userids The list of users.
|
||||
* @return $this
|
||||
*/
|
||||
protected function set_userids(array $userids) : userlist_base {
|
||||
$this->userids = array_values(array_unique($userids));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of additional userids.
|
||||
*
|
||||
* @param array $userids The list of users.
|
||||
* @return $this
|
||||
*/
|
||||
protected function add_userids(array $userids) : userlist_base {
|
||||
$this->set_userids(array_merge($this->get_userids(), $userids));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of user IDs that relate to this request.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function get_userids() : array {
|
||||
return $this->userids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete list of user objects that relate to this request.
|
||||
*
|
||||
* @return \stdClass[]
|
||||
*/
|
||||
public function get_users() : array {
|
||||
$users = [];
|
||||
foreach ($this->userids as $userid) {
|
||||
if ($user = \core_user::get_user($userid)) {
|
||||
$users[] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the component for this userlist.
|
||||
*
|
||||
* @param string $component the frankenstyle component name.
|
||||
* @return $this
|
||||
*/
|
||||
protected function set_component($component) : userlist_base {
|
||||
$this->component = $component;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the component to which this userlist belongs.
|
||||
*
|
||||
* @return string the component name associated with this userlist.
|
||||
*/
|
||||
public function get_component() : string {
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current user.
|
||||
*
|
||||
* @return \user
|
||||
*/
|
||||
public function current() {
|
||||
$user = \core_user::get_user($this->userids[$this->iteratorposition]);
|
||||
|
||||
if (false === $user) {
|
||||
// This user was not found.
|
||||
unset($this->userids[$this->iteratorposition]);
|
||||
|
||||
// Check to see if there are any more users left.
|
||||
if ($this->count()) {
|
||||
// Move the pointer to the next record and try again.
|
||||
$this->next();
|
||||
$user = $this->current();
|
||||
} else {
|
||||
// There are no more context ids left.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function key() {
|
||||
return $this->iteratorposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next user in the list.
|
||||
*/
|
||||
public function next() {
|
||||
++$this->iteratorposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current position is valid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid() {
|
||||
return isset($this->userids[$this->iteratorposition]) && $this->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind to the first found user.
|
||||
*
|
||||
* The list of users is uniqued during the rewind.
|
||||
* The rewind is called at the start of most iterations.
|
||||
*/
|
||||
public function rewind() {
|
||||
$this->iteratorposition = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of users.
|
||||
*/
|
||||
public function count() {
|
||||
return count($this->userids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context for this userlist
|
||||
*
|
||||
* @return \context
|
||||
*/
|
||||
public function get_context() : \context {
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This file defines the userlist_collection class object.
|
||||
*
|
||||
* The userlist_collection is used to organize a collection of userlists.
|
||||
*
|
||||
* @package core_privacy
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_privacy\local\request;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* A collection of userlist items.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class userlist_collection implements \Iterator, \Countable {
|
||||
|
||||
/**
|
||||
* @var \context $context The context that the userlist collection belongs to.
|
||||
*/
|
||||
protected $context = null;
|
||||
|
||||
/**
|
||||
* @var array $userlists the internal array of userlist objects.
|
||||
*/
|
||||
protected $userlists = [];
|
||||
|
||||
/**
|
||||
* @var int Current position of the iterator.
|
||||
*/
|
||||
protected $iteratorposition = 0;
|
||||
|
||||
/**
|
||||
* Constructor to create a new userlist_collection.
|
||||
*
|
||||
* @param \context $context The context to which this collection belongs.
|
||||
*/
|
||||
public function __construct(\context $context) {
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the context that this collection relates to.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_context() : \context {
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a userlist to this collection.
|
||||
*
|
||||
* @param userlist_base $userlist the userlist to export.
|
||||
* @return $this
|
||||
*/
|
||||
public function add_userlist(userlist_base $userlist) : userlist_collection {
|
||||
$component = $userlist->get_component();
|
||||
if (isset($this->userlists[$component])) {
|
||||
throw new \moodle_exception("A userlist has already been added for the '{$component}' component");
|
||||
}
|
||||
|
||||
$this->userlists[$component] = $userlist;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the userlists in this collection.
|
||||
*
|
||||
* @return array the associative array of userlists in this collection, indexed by component name.
|
||||
* E.g. mod_assign => userlist, core_comment => userlist.
|
||||
*/
|
||||
public function get_userlists() : array {
|
||||
return $this->userlists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the userlist for the specified component.
|
||||
*
|
||||
* @param string $component the frankenstyle name of the component to fetch for.
|
||||
* @return userlist_base|null
|
||||
*/
|
||||
public function get_userlist_for_component(string $component) {
|
||||
if (isset($this->userlists[$component])) {
|
||||
return $this->userlists[$component];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current contexlist.
|
||||
*
|
||||
* @return \user
|
||||
*/
|
||||
public function current() {
|
||||
$key = $this->get_key_from_position();
|
||||
return $this->userlists[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function key() {
|
||||
return $this->get_key_from_position();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next user in the list.
|
||||
*/
|
||||
public function next() {
|
||||
++$this->iteratorposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current position is valid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid() {
|
||||
return ($this->iteratorposition < count($this->userlists));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind to the first found user.
|
||||
*
|
||||
* The list of users is uniqued during the rewind.
|
||||
* The rewind is called at the start of most iterations.
|
||||
*/
|
||||
public function rewind() {
|
||||
$this->iteratorposition = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for the current iterator position.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_key_from_position() {
|
||||
$keylist = array_keys($this->userlists);
|
||||
if (isset($keylist[$this->iteratorposition])) {
|
||||
return $keylist[$this->iteratorposition];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of users.
|
||||
*/
|
||||
public function count() {
|
||||
return count($this->userlists);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ use core_privacy\local\metadata\null_provider;
|
||||
use core_privacy\local\request\context_aware_provider;
|
||||
use core_privacy\local\request\contextlist_collection;
|
||||
use core_privacy\local\request\core_user_data_provider;
|
||||
use core_privacy\local\request\core_userlist_provider;
|
||||
use core_privacy\local\request\data_provider;
|
||||
use core_privacy\local\request\user_preference_provider;
|
||||
use \core_privacy\local\metadata\provider as metadata_provider;
|
||||
@@ -255,6 +256,46 @@ class manager {
|
||||
return $clcollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of users for all components in the specified context.
|
||||
*
|
||||
* @param \context $context The context to search
|
||||
* @return userlist_collection the collection of userlist items for the respective components.
|
||||
*/
|
||||
public function get_users_in_context(\context $context) : \core_privacy\local\request\userlist_collection {
|
||||
$progress = static::get_log_tracer();
|
||||
|
||||
$components = $this->get_component_list();
|
||||
$a = (object) [
|
||||
'total' => count($components),
|
||||
'progress' => 0,
|
||||
'component' => '',
|
||||
'datetime' => userdate(time()),
|
||||
];
|
||||
$collection = new \core_privacy\local\request\userlist_collection($context);
|
||||
|
||||
$progress->output(get_string('trace:fetchcomponents', 'core_privacy', $a), 1);
|
||||
foreach ($components as $component) {
|
||||
$a->component = $component;
|
||||
$a->progress++;
|
||||
$a->datetime = userdate(time());
|
||||
$progress->output(get_string('trace:preprocessingcomponent', 'core_privacy', $a), 2);
|
||||
$userlist = new local\request\userlist($context, $component);
|
||||
|
||||
$this->handled_component_class_callback($component, core_userlist_provider::class, 'get_users_in_context', [$userlist]);
|
||||
|
||||
// Add contexts that the component may not know about.
|
||||
\core_privacy\local\request\helper::add_shared_users_to_userlist($userlist);
|
||||
|
||||
if (count($userlist)) {
|
||||
$collection->add_userlist($userlist);
|
||||
}
|
||||
}
|
||||
$progress->output(get_string('trace:done', 'core_privacy'), 1);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user data for the specified approved_contextlist items.
|
||||
*
|
||||
@@ -380,6 +421,48 @@ class manager {
|
||||
$progress->output(get_string('trace:done', 'core_privacy'), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all user data for all specified users in a context.
|
||||
*
|
||||
* @param \core_privacy\local\request\userlist_collection $collection
|
||||
*/
|
||||
public function delete_data_for_users_in_context(\core_privacy\local\request\userlist_collection $collection) {
|
||||
$progress = static::get_log_tracer();
|
||||
|
||||
$a = (object) [
|
||||
'contextid' => $collection->get_context()->id,
|
||||
'total' => count($collection),
|
||||
'progress' => 0,
|
||||
'component' => '',
|
||||
'datetime' => userdate(time()),
|
||||
];
|
||||
|
||||
// Delete the data.
|
||||
$progress->output(get_string('trace:deletingapprovedusers', 'core_privacy', $a), 1);
|
||||
foreach ($collection as $userlist) {
|
||||
if (!$userlist instanceof \core_privacy\local\request\approved_userlist) {
|
||||
throw new \moodle_exception('The supplied userlist must be an approved_userlist');
|
||||
}
|
||||
|
||||
$component = $userlist->get_component();
|
||||
$a->component = $component;
|
||||
$a->progress++;
|
||||
$a->datetime = userdate(time());
|
||||
|
||||
if (empty($userlist)) {
|
||||
// This really shouldn't happen!
|
||||
continue;
|
||||
}
|
||||
|
||||
$progress->output(get_string('trace:processingcomponent', 'core_privacy', $a), 2);
|
||||
|
||||
$this->handled_component_class_callback($component, core_userlist_provider::class,
|
||||
'delete_data_for_users', [$userlist]);
|
||||
}
|
||||
|
||||
$progress->output(get_string('trace:done', 'core_privacy'), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all use data which matches the specified deletion criteria.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit Tests for the approved userlist Class
|
||||
*
|
||||
* @package core_privacy
|
||||
* @category test
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Tests for the \core_privacy API's approved userlist functionality.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class approved_userlist_test extends advanced_testcase {
|
||||
/**
|
||||
* The approved userlist should not be modifiable once set.
|
||||
*/
|
||||
public function test_default_values_set() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
$u3 = $this->getDataGenerator()->create_user();
|
||||
$u4 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$context = \context_system::instance();
|
||||
$component = 'core_privacy';
|
||||
|
||||
$uut = new approved_userlist($context, $component, [$u1->id, $u2->id]);
|
||||
|
||||
$this->assertEquals($context, $uut->get_context());
|
||||
$this->assertEquals($component, $uut->get_component());
|
||||
|
||||
$expected = [
|
||||
$u1->id,
|
||||
$u2->id,
|
||||
];
|
||||
sort($expected);
|
||||
|
||||
$result = $uut->get_userids();
|
||||
sort($result);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function test_create_from_userlist() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
$u3 = $this->getDataGenerator()->create_user();
|
||||
$u4 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$context = \context_system::instance();
|
||||
$component = 'core_privacy';
|
||||
|
||||
$sourcelist = new userlist($context, $component);
|
||||
$sourcelist->add_users([$u1->id, $u3->id]);
|
||||
|
||||
$expected = [
|
||||
$u1->id,
|
||||
$u3->id,
|
||||
];
|
||||
sort($expected);
|
||||
|
||||
$approvedlist = approved_userlist::create_from_userlist($sourcelist);
|
||||
|
||||
$this->assertEquals($component, $approvedlist->get_component());
|
||||
$this->assertEquals($context, $approvedlist->get_context());
|
||||
|
||||
$result = $approvedlist->get_userids();
|
||||
sort($result);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit Tests for the abstract userlist Class
|
||||
*
|
||||
* @package core_privacy
|
||||
* @category test
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
use \core_privacy\local\request\userlist_base;
|
||||
|
||||
/**
|
||||
* Tests for the \core_privacy API's userlist base functionality.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class userlist_base_test extends advanced_testcase {
|
||||
/**
|
||||
* Ensure that get_userids returns the list of unique userids.
|
||||
*
|
||||
* @dataProvider get_userids_provider
|
||||
* @param array $input List of user IDs
|
||||
* @param array $expected list of userids
|
||||
* @param int $count Expected count
|
||||
*/
|
||||
public function test_get_userids($input, $expected, $count) {
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids($input);
|
||||
|
||||
$result = $uut->get_userids();
|
||||
$this->assertCount($count, $result);
|
||||
|
||||
// Note: Array order is not guaranteed and should not matter.
|
||||
foreach ($expected as $userid) {
|
||||
$this->assertNotFalse(array_search($userid, $result));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for the list of userids.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_userids_provider() {
|
||||
return [
|
||||
'basic' => [
|
||||
[1, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
5,
|
||||
],
|
||||
'duplicates' => [
|
||||
[1, 1, 2, 2, 3, 4, 5],
|
||||
[1, 2, 3, 4, 5],
|
||||
5,
|
||||
],
|
||||
'Mixed order with duplicates' => [
|
||||
[5, 4, 2, 5, 4, 1, 3, 4, 1, 5, 5, 5, 2, 4, 1, 2],
|
||||
[1, 2, 3, 4, 5],
|
||||
5,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that get_users returns the correct list of users.
|
||||
*/
|
||||
public function test_get_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$users = [];
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$users[$user->id] = $user;
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$users[$user->id] = $user;
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$users[$user->id] = $user;
|
||||
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
$ids = array_keys($users);
|
||||
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids($ids);
|
||||
|
||||
$result = $uut->get_users();
|
||||
|
||||
sort($users);
|
||||
sort($result);
|
||||
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($users, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the userlist_base is countable.
|
||||
*
|
||||
* @dataProvider get_userids_provider
|
||||
* @param array $input List of user IDs
|
||||
* @param array $expected list of userids
|
||||
* @param int $count Expected count
|
||||
*/
|
||||
public function test_countable($input, $expected, $count) {
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids($input);
|
||||
|
||||
$this->assertCount($count, $uut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the userlist_base iterates over the set of users.
|
||||
*/
|
||||
public function test_user_iteration() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$users = [];
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$users[$user->id] = $user;
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$users[$user->id] = $user;
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$users[$user->id] = $user;
|
||||
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
$ids = array_keys($users);
|
||||
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids($ids);
|
||||
|
||||
foreach ($uut as $key => $user) {
|
||||
$this->assertTrue(isset($users[$user->id]));
|
||||
$this->assertEquals($users[$user->id], $user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a deleted user is still returned.
|
||||
* If a user has data then it still must be deleted, even if they are deleted.
|
||||
*/
|
||||
public function test_current_user_one_user() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids([$user->id]);
|
||||
|
||||
$this->assertCount(1, $uut);
|
||||
$this->assertEquals($user, $uut->current());
|
||||
|
||||
delete_user($user);
|
||||
$u = $uut->current();
|
||||
$this->assertEquals($user->id, $u->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an invalid user returns no entry.
|
||||
*/
|
||||
public function test_current_user_invalid() {
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids([-100]);
|
||||
|
||||
$this->assertCount(1, $uut);
|
||||
$this->assertNull($uut->current());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that where an invalid user is listed, the next user in the list is returned instead.
|
||||
*/
|
||||
public function test_current_user_two_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$uut->set_userids([-100, $u1->id]);
|
||||
|
||||
$this->assertCount(2, $uut);
|
||||
$this->assertEquals($u1, $uut->current());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the component specified in the constructor is used and available.
|
||||
*/
|
||||
public function test_set_component_in_constructor() {
|
||||
$uut = new test_userlist_base(\context_system::instance(), 'core_tests');
|
||||
$this->assertEquals('core_tests', $uut->get_component());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the context specified in the constructor is available.
|
||||
*/
|
||||
public function test_set_context_in_constructor() {
|
||||
$context = \context_user::instance(\core_user::get_user_by_username('admin')->id);
|
||||
|
||||
$uut = new test_userlist_base($context, 'core_tests');
|
||||
$this->assertEquals($context, $uut->get_context());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A test class extending the userlist_base allowing setting of the userids.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class test_userlist_base extends userlist_base {
|
||||
/**
|
||||
* Set the contextids for the test class.
|
||||
*
|
||||
* @param int[] $contexids The list of contextids to use.
|
||||
*/
|
||||
public function set_userids(array $userids) : userlist_base {
|
||||
return parent::set_userids($userids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Unit Tests for a the collection of userlists class
|
||||
*
|
||||
* @package core_privacy
|
||||
* @category test
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
use \core_privacy\local\request\userlist_collection;
|
||||
use \core_privacy\local\request\userlist;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
|
||||
/**
|
||||
* Tests for the \core_privacy API's userlist collection functionality.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class userlist_collection_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* A userlist_collection should support the userlist type.
|
||||
*/
|
||||
public function test_supports_userlist() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$userlist = new userlist($cut, 'core_privacy');
|
||||
$uut->add_userlist($userlist);
|
||||
|
||||
$this->assertCount(1, $uut->get_userlists());
|
||||
}
|
||||
|
||||
/**
|
||||
* A userlist_collection should support the approved_userlist type.
|
||||
*/
|
||||
public function test_supports_approved_userlist() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$userlist = new approved_userlist($cut, 'core_privacy', [1, 2, 3]);
|
||||
$uut->add_userlist($userlist);
|
||||
|
||||
$this->assertCount(1, $uut->get_userlists());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that get_userlist_for_component returns the correct userlist.
|
||||
*/
|
||||
public function test_get_userlist_for_component() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$privacy = new userlist($cut, 'core_privacy');
|
||||
$uut->add_userlist($privacy);
|
||||
|
||||
$test = new userlist($cut, 'core_tests');
|
||||
$uut->add_userlist($test);
|
||||
|
||||
// Note: This uses assertSame rather than assertEquals.
|
||||
// The former checks the actual object, whilst assertEquals only checks that they look the same.
|
||||
$this->assertSame($privacy, $uut->get_userlist_for_component('core_privacy'));
|
||||
$this->assertSame($test, $uut->get_userlist_for_component('core_tests'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that get_userlist_for_component does not die horribly when querying a non-existent component.
|
||||
*/
|
||||
public function test_get_userlist_for_component_not_found() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$this->assertNull($uut->get_userlist_for_component('core_tests'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a duplicate userlist in the collection throws an Exception.
|
||||
*/
|
||||
public function test_duplicate_addition_throws() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$userlist = new userlist($cut, 'core_privacy');
|
||||
$uut->add_userlist($userlist);
|
||||
|
||||
$this->expectException('moodle_exception');
|
||||
$uut->add_userlist($userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the userlist_collection is countable.
|
||||
*/
|
||||
public function test_countable() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$uut->add_userlist(new userlist($cut, 'core_privacy'));
|
||||
$uut->add_userlist(new userlist($cut, 'core_tests'));
|
||||
|
||||
$this->assertCount(2, $uut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the userlist_collection iterates over the set of userlists.
|
||||
*/
|
||||
public function test_iteration() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$testdata = [];
|
||||
|
||||
$privacy = new userlist($cut, 'core_privacy');
|
||||
$uut->add_userlist($privacy);
|
||||
$testdata['core_privacy'] = $privacy;
|
||||
|
||||
$test = new userlist($cut, 'core_tests');
|
||||
$uut->add_userlist($test);
|
||||
$testdata['core_tests'] = $test;
|
||||
|
||||
$another = new userlist($cut, 'privacy_another');
|
||||
$uut->add_userlist($another);
|
||||
$testdata['privacy_another'] = $another;
|
||||
|
||||
foreach ($uut as $component => $list) {
|
||||
$this->assertEquals($testdata[$component], $list);
|
||||
}
|
||||
|
||||
$this->assertCount(3, $uut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the context is correctly returned.
|
||||
*/
|
||||
public function test_get_context() {
|
||||
$cut = \context_system::instance();
|
||||
$uut = new userlist_collection($cut);
|
||||
|
||||
$this->assertSame($cut, $uut->get_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/>.
|
||||
|
||||
/**
|
||||
* Unit Tests for the approved userlist Class
|
||||
*
|
||||
* @package core_privacy
|
||||
* @category test
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
||||
use \core_privacy\local\request\userlist;
|
||||
|
||||
/**
|
||||
* Tests for the \core_privacy API's approved userlist functionality.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class userlist_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that valid SQL results in the relevant users being added.
|
||||
*/
|
||||
public function test_add_from_sql() {
|
||||
global $DB;
|
||||
|
||||
$sql = "SELECT c.id FROM {user} c";
|
||||
$params = [];
|
||||
$allusers = $DB->get_records_sql($sql, $params);
|
||||
|
||||
$uut = new userlist(\context_system::instance(), 'core_privacy');
|
||||
$uut->add_from_sql('id', $sql, $params);
|
||||
|
||||
$this->assertCount(count($allusers), $uut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that adding a single user adds that user.
|
||||
*/
|
||||
public function test_add_user() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$uut = new userlist(\context_system::instance(), 'core_privacy');
|
||||
$uut->add_user($u1->id);
|
||||
|
||||
$this->assertCount(1, $uut);
|
||||
$this->assertEquals($uut->current(), $u1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that adding multiple users by ID adds those users.
|
||||
*/
|
||||
public function test_add_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
$u3 = $this->getDataGenerator()->create_user();
|
||||
$expected = [$u1->id, $u3->id];
|
||||
|
||||
$uut = new userlist(\context_system::instance(), 'core_privacy');
|
||||
$uut->add_users([$u1->id, $u3->id]);
|
||||
|
||||
$this->assertCount(2, $uut);
|
||||
|
||||
foreach ($uut as $user) {
|
||||
$this->assertNotFalse(array_search($user->id, $expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user