芝麻web文件管理V1.00
编辑当前文件:/home2/sdektunc/.trash/cepali/admin/tool/dataprivacy/classes/expired_contexts_manager.php
. /** * 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} dpctx LEFT JOIN {context} ctx ON ctx.id = dpctx.contextid WHERE ctx.id IS NULL"; $orphaned = $DB->get_recordset_sql($sql); foreach ($orphaned as $orphan) { $expiredcontext = new expired_context(0, $orphan); $expiredcontext->delete(); } // Delete any child of a user context. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); $params = [ 'contextuser' => CONTEXT_USER, ]; $sql = "SELECT dpctx.* FROM {tool_dataprivacy_ctxexpired} dpctx WHERE dpctx.contextid IN ( SELECT ctx.id FROM {context} ctxuser JOIN {context} ctx ON ctx.path LIKE {$parentpath} WHERE ctxuser.contextlevel = :contextuser )"; $userchildren = $DB->get_recordset_sql($sql, $params); foreach ($userchildren as $child) { $expiredcontext = new expired_context(0, $child); $expiredcontext->delete(); } } /** * Get the full nested set of expiry data relating to all contexts. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info($contextpath = '') : 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 $fields FROM {context} ctx JOIN ( SELECT c.enddate AS expirydate, subctx.path FROM {context} subctx JOIN {course} c ON subctx.contextlevel = :contextlevel AND subctx.instanceid = c.id AND c.format != 'site' ) ctxcourse ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx ON dpctx.contextid = ctx.id LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid {$where} ORDER BY ctx.path DESC"; return self::get_nested_expiry_info_from_sql($sql, $params); } /** * Get the full nested set of expiry data. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info_for_user($contextpath = '') : 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 userdeleted FROM {context} ctx JOIN {user} u ON ctx.instanceid = u.id LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx ON dpctx.contextid = ctx.id LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid WHERE ctx.contextlevel = :contextlevel {$where} ORDER BY ctx.path DESC"; return self::get_nested_expiry_info_from_sql($sql, $params); } /** * Get the full nested set of expiry data given appropriate SQL. * Only contexts which have expired will be included. * * @param string $sql The SQL used to select the nested information. * @param array $params The params required by the SQL. * @return \stdClass[] */ protected static function get_nested_expiry_info_from_sql(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, $fields FROM {context} ctxuser JOIN {context} ctx ON ctx.path LIKE {$parentpath} WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextid ORDER BY ctx.path DESC"; $children = $DB->get_recordset_sql($sql, $params); foreach ($children as $child) { \context_helper::preload_from_record($child); $context = \context::instance_by_id($child->id); $privacymanager->delete_data_for_all_users_in_context($context); } $children->close(); // Delete all unprotected data that the user holds. $approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id); $contextlistcollection = $privacymanager->get_contexts_for_userid($user->id); foreach ($contextlistcollection as $contextlist) { $contextids = []; $approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist( $user, $contextlist->get_component(), $contextlist->get_contextids() )); } $privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress()); // Delete the user context. $context = \context::instance_by_id($expiredctx->get('contextid')); $privacymanager->delete_data_for_all_users_in_context($context); // This user is now fully expired - finish by deleting the user. delete_user($user); } /** * Whether end dates are required on all courses in order for a user to be expired from them. * * @return bool */ protected static function require_all_end_dates_for_user_deletion() : bool { $requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion'); return !empty($requireenddate); } /** * Check that the requirements to start deleting contexts are satisified. * * @return bool */ protected function check_requirements() { 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; } }