Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Expired contexts manager.** @package tool_dataprivacy* @copyright 2018 David Monllao* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/namespace tool_dataprivacy;use core_privacy\manager;use tool_dataprivacy\expired_context;defined('MOODLE_INTERNAL') || die();/*** Expired contexts manager.** @copyright 2018 David Monllao* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class expired_contexts_manager {/*** Number of deleted contexts for each scheduled task run.*/const DELETE_LIMIT = 200;/** @var progress_trace The log progress tracer */protected $progresstracer = null;/** @var manager The privacy manager */protected $manager = null;/** @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];}/*** Clear old and stale records.*/protected static function clear_old_records() {global $DB;$sql = "SELECT dpctx.*FROM {tool_dataprivacy_ctxexpired} dpctxLEFT JOIN {context} ctx ON ctx.id = dpctx.contextidWHERE ctx.id IS NULL";$orphaned = $DB->get_recordset_sql($sql);foreach ($orphaned as $orphan) {$expiredcontext = new expired_context(0, $orphan);$expiredcontext->delete();}// Delete any child of a user context.$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");$params = ['contextuser' => CONTEXT_USER,];$sql = "SELECT dpctx.*FROM {tool_dataprivacy_ctxexpired} dpctxWHERE dpctx.contextid IN (SELECT ctx.idFROM {context} ctxuserJOIN {context} ctx ON ctx.path LIKE {$parentpath}WHERE ctxuser.contextlevel = :contextuser)";$userchildren = $DB->get_recordset_sql($sql, $params);foreach ($userchildren as $child) {$expiredcontext = new expired_context(0, $child);$expiredcontext->delete();}}/*** Get the full nested set of expiry data relating to all contexts.** @param string $contextpath A contexpath to restrict results to* @return \stdClass[]*/protected static function get_nested_expiry_info($contextpath = ''): array {$coursepaths = self::get_nested_expiry_info_for_courses($contextpath);$userpaths = self::get_nested_expiry_info_for_user($contextpath);return array_merge($coursepaths, $userpaths);}/*** Get the full nested set of expiry data relating to course-related contexts.** @param string $contextpath A contexpath to restrict results to* @return \stdClass[]*/protected static function get_nested_expiry_info_for_courses($contextpath = ''): array {global $DB;$contextfields = \context_helper::get_preload_record_columns_sql('ctx');$expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');$purposefields = 'dpctx.purposeid';$coursefields = 'ctxcourse.expirydate AS expirydate';$fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $coursefields, $purposefields]);// We want all contexts at course-dependant levels.$parentpath = $DB->sql_concat('ctxcourse.path', "'/%'");// This SQL query returns all course-dependant contexts (including the course context)// which course end date already passed.// This is ordered by the context path in reverse order, which will give the child nodes before any parent node.$params = ['contextlevel' => CONTEXT_COURSE,];$where = '';if (!empty($contextpath)) {$where = "WHERE (ctx.path = :pathmatchexact OR ctx.path LIKE :pathmatchchildren)";$params['pathmatchexact'] = $contextpath;$params['pathmatchchildren'] = "{$contextpath}/%";}$sql = "SELECT $fieldsFROM {context} ctxJOIN (SELECT c.enddate AS expirydate, subctx.pathFROM {context} subctxJOIN {course} cON subctx.contextlevel = :contextlevelAND subctx.instanceid = c.idAND c.format != 'site') ctxcourseON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.pathLEFT JOIN {tool_dataprivacy_ctxinstance} dpctxON dpctx.contextid = ctx.idLEFT JOIN {tool_dataprivacy_ctxexpired} expiredctxON ctx.id = expiredctx.contextid{$where}ORDER BY ctx.path DESC";return self::get_nested_expiry_info_from_sql($sql, $params);}/*** Get the full nested set of expiry data.** @param string $contextpath A contexpath to restrict results to* @return \stdClass[]*/protected static function get_nested_expiry_info_for_user($contextpath = ''): array {global $DB;$contextfields = \context_helper::get_preload_record_columns_sql('ctx');$expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx');$purposefields = 'dpctx.purposeid';$userfields = 'u.lastaccess AS expirydate';$fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $userfields, $purposefields]);// We want all contexts at user-dependant levels.$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");// This SQL query returns all user-dependant contexts (including the user context)// This is ordered by the context path in reverse order, which will give the child nodes before any parent node.$params = ['contextlevel' => CONTEXT_USER,];$where = '';if (!empty($contextpath)) {$where = "AND ctx.path = :pathmatchexact";$params['pathmatchexact'] = $contextpath;}$sql = "SELECT $fields, u.deleted AS userdeletedFROM {context} ctxJOIN {user} u ON ctx.instanceid = u.idLEFT JOIN {tool_dataprivacy_ctxinstance} dpctxON dpctx.contextid = ctx.idLEFT JOIN {tool_dataprivacy_ctxexpired} expiredctxON ctx.id = expiredctx.contextidWHERE ctx.contextlevel = :contextlevel {$where}ORDER BY ctx.path DESC";return self::get_nested_expiry_info_from_sql($sql, $params);}/*** Get the full nested set of expiry data given appropriate SQL.* Only contexts which have expired will be included.** @param string $sql The SQL used to select the nested information.* @param array $params The params required by the SQL.* @return \stdClass[]*/protected static function get_nested_expiry_info_from_sql(string $sql, array $params): array {global $DB;$fulllist = $DB->get_recordset_sql($sql, $params);$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);if (!self::is_eligible_for_deletion($pathstoskip, $context)) {// We should skip this context, and therefore all of it's children.$datalist = array_filter($datalist, function($data, $path) use ($context) {// Remove any child of this context.// Technically this should never be fulfilled because the query is ordered in path DESC, but is kept// in to be certain.return (false === strpos($path, "{$context->path}/"));}, ARRAY_FILTER_USE_BOTH);if ($record->expiredctxid) {// There was previously an expired context record.// Delete it to be on the safe side.$expiredcontext = new expired_context(null, expired_context::extract_record($record, 'expiredctx'));$expiredcontext->delete();}continue;}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) {continue;}$expiryinfo->merge_with_child($data->info);}$datalist[$context->path] = (object) ['context' => $context,'record' => $record,'purpose' => $purpose,'info' => $expiryinfo,];}$fulllist->close();return $datalist;}/*** Check whether the supplied context would be elible for deletion.** @param array $pathstoskip A set of paths which should be skipped* @param \context $context* @return bool*/protected static function is_eligible_for_deletion(array &$pathstoskip, \context $context): bool {$shouldskip = false;// Check whether any of the child contexts are ineligble.$shouldskip = !empty(array_filter($pathstoskip, function($path) use ($context) {// If any child context has already been skipped then it will appear in this list.// Since paths include parents, test if the context under test appears as the haystack in the skipped// context's needle.return false !== (strpos($context->path, $path));}));if (!$shouldskip && $context instanceof \context_user) {$shouldskip = !self::are_user_context_dependencies_expired($context);}if ($shouldskip) {// Add this to the list of contexts to skip for parentage checks.$pathstoskip[] = $context->path;}return !$shouldskip;}/*** Deletes the expired contexts.** @return int[] The number of deleted contexts.*/public function process_approved_deletions(): array {$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.$expiredctx->delete();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 {$coursecount++;}$totalprocessed++;if ($totalprocessed >= $this->get_delete_limit()) {break;}}}return [$coursecount, $usercount];}/*** Deletes user data from the provided context.** @param expired_context $expiredctx* @return \context|false*/protected function delete_expired_context(expired_context $expiredctx) {$context = \context::instance_by_id($expiredctx->get('contextid'));$this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true));// Update the expired_context and verify that it is still ready for deletion.$expiredctx = $this->update_expired_context($expiredctx);if (empty($expiredctx)) {$this->get_progress()->output("Context has changed since approval and is no longer pending approval. Skipping", 1);return false;}if (!$expiredctx->can_process_deletion()) {// This only happens if the record was updated after being first fetched.$this->get_progress()->output("Context has changed since approval and must be re-approved. Skipping", 1);$expiredctx->set('status', expired_context::STATUS_EXPIRED);$expiredctx->save();return false;}$privacymanager = $this->get_privacy_manager();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.$expiredctx->set('status', expired_context::STATUS_CLEANED);$expiredctx->save();return $context;}/*** Deletes user data from the provided user context.** @param expired_context $expiredctx*/protected function delete_expired_user_context(expired_context $expiredctx) {global $DB;$contextid = $expiredctx->get('contextid');$context = \context::instance_by_id($contextid);$user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);$privacymanager = $this->get_privacy_manager();// Delete all child contexts of the user context.$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");$params = ['contextlevel' => CONTEXT_USER,'contextid' => $expiredctx->get('contextid'),];$fields = \context_helper::get_preload_record_columns_sql('ctx');$sql = "SELECT ctx.id, $fieldsFROM {context} ctxuserJOIN {context} ctx ON ctx.path LIKE {$parentpath}WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextidORDER BY ctx.path DESC";$children = $DB->get_recordset_sql($sql, $params);foreach ($children as $child) {\context_helper::preload_from_record($child);$context = \context::instance_by_id($child->id);$privacymanager->delete_data_for_all_users_in_context($context);}$children->close();// Delete all unprotected data that the user holds.$approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);$contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);foreach ($contextlistcollection as $contextlist) {$contextids = [];$approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist($user,$contextlist->get_component(),$contextlist->get_contextids()));}$privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress());// Delete the user context.$context = \context::instance_by_id($expiredctx->get('contextid'));$privacymanager->delete_data_for_all_users_in_context($context);// This user is now fully expired - finish by deleting the user.delete_user($user);}/*** Whether end dates are required on all courses in order for a user to be expired from them.** @return bool*/protected static function require_all_end_dates_for_user_deletion(): bool {$requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion');return !empty($requireenddate);}/*** Check that the requirements to start deleting contexts are satisified.** @return bool*/protected function check_requirements() {if (!data_registry::defaults_set()) {return false;}return true;}/*** Check whether a date is beyond the specified period.** @param string $period The Expiry Period* @param int $comparisondate The date for comparison* @return bool*/protected static function has_expired(string $period, int $comparisondate): bool {$dt = new \DateTime();$dt->setTimestamp($comparisondate);$dt->add(new \DateInterval($period));return (time() >= $dt->getTimestamp());}/*** Get the expiry info object for the specified purpose and comparison date.** @param purpose $purpose The purpose of this context* @param int $comparisondate The date for comparison* @return expiry_info*/protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0): expiry_info {$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, $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);}}/*** Update or delete the expired_context from the expiry_info object.* This function depends upon the data structure returned from get_nested_expiry_info.** If the context is expired in any way, then an expired_context will be returned, otherwise null will be returned.** @param \stdClass $expiryrecord* @return expired_context|null*/protected function update_from_expiry_info(\stdClass $expiryrecord) {if ($isanyexpired = $expiryrecord->info->is_any_expired()) {// The context is expired in some fashion.// Create or update as required.if ($expiryrecord->record->expiredctxid) {$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));$expiredcontext->update_from_expiry_info($expiryrecord->info);if ($expiredcontext->is_complete()) {return null;}} else {$expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);}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.if ($expiryrecord->record->expiredctxid) {// There was previously an expired context record, but it is no longer relevant.// Delete it to be on the safe side.$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));$expiredcontext->delete();}return null;}}/*** Update the expired context record.** Note: You should use the return value as the provided value will be used to fetch data only.** @param expired_context $expiredctx The record to update* @return expired_context|null*/protected function update_expired_context(expired_context $expiredctx) {// Fetch the context from the expired_context record.$context = \context::instance_by_id($expiredctx->get('contextid'));// Fetch the current nested expiry data.$expiryrecords = self::get_nested_expiry_info($context->path);if (empty($expiryrecords[$context->path])) {$expiredctx->delete();return null;}// Refresh the record.// Note: Use the returned expiredctx.$expiredctx = $this->update_from_expiry_info($expiryrecords[$context->path]);if (empty($expiredctx)) {return null;}if (!$context instanceof \context_user) {// Where the target context is not a user, we check all children of the context.// The expiryrecords array only contains children, fetched from the get_nested_expiry_info call above.// No need to check that these _are_ children.foreach ($expiryrecords as $expiryrecord) {if ($expiryrecord->context->id === $context->id) {// This is record for the context being tested that we checked earlier.continue;}if (empty($expiryrecord->record->expiredctxid)) {// There is no expired context record for this context.// If there is no record, then this context cannot have been approved for removal.return null;}// Fetch the expired_context object for this record.// This needs to be updated from the expiry_info data too as there may be child changes to consider.$expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx'));$expiredcontext->update_from_expiry_info($expiryrecord->info);if (!$expiredcontext->is_complete()) {return null;}}}return $expiredctx;}/*** 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) {// This is a context within a course. Check whether _this context_ is expired as a function of a course.return self::is_course_context_expired($context);}if ($parent instanceof \context_user) {// This is a context within a user. Check whether the _user_ has expired.return self::are_user_context_dependencies_expired($parent);}}return false;}/*** Check whether the course has expired.** @param \stdClass $course* @return bool*/protected static function is_course_expired(\stdClass $course): bool {$context = \context_course::instance($course->id);return self::is_course_context_expired($context);}/*** Determine whether the supplied course-related context has expired.* Note: This is not necessarily a _course_ context, but a context which is _within_ a course.** @param \context $context* @return bool*/protected static function is_course_context_expired(\context $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 {// User/course contexts can't expire if no purpose is set in the system context.if (!data_registry::defaults_set()) {return false;}$parents = $context->get_parent_contexts(true);foreach ($parents as $parent) {if ($parent instanceof \context_course) {// This is a context within a course. Check whether _this context_ is expired as a function of a course.return self::is_course_context_expired_or_unprotected_for_user($context, $user);}if ($parent instanceof \context_user) {// This is a context within a user. Check whether the _user_ has expired.return self::are_user_context_dependencies_expired($parent);}}return false;}/*** Determine whether the supplied course-related context has expired, or is unprotected.* Note: This is not necessarily a _course_ context, but a context which is _within_ a course.** @param \context $context* @param \stdClass $user* @return bool*/protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) {if ($context->get_course_context()->instanceid == SITEID) {// The is an activity in the site course (front page).$purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose');$info = static::get_expiry_info($purpose);} else {$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.** @return manager*/protected function get_privacy_manager(): manager {if (null === $this->manager) {$this->manager = new manager();$this->manager->set_observer(new \tool_dataprivacy\manager_observer());}return $this->manager;}/*** Fetch the limit for the maximum number of contexts to delete in one session.** @return int*/protected function get_delete_limit(): int {return self::DELETE_LIMIT;}/*** Get the progress tracer.** @return \progress_trace*/protected function get_progress(): \progress_trace {if (null === $this->progresstracer) {$this->set_progress(new \text_progress_trace());}return $this->progresstracer;}/*** Set a specific tracer for the task.** @param \progress_trace $trace* @return $this*/public function set_progress(\progress_trace $trace): expired_contexts_manager {$this->progresstracer = $trace;return $this;}}