user_expire-8.x-1.x-dev/user_expire.module
user_expire.module
<?php
/**
* @file
* Main module file for User expire module.
*/
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\DeprecationHelper;
/**
* Implements hook_help().
*/
function user_expire_help($route_name, RouteMatchInterface $route_match) {
$output = '';
switch ($route_name) {
// Main module help for the user_expire module.
case 'help.page.user_expire':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<dt>' . t('This module allows an administrator to define a date on which to expire a specific user account or to define a period at a role level where inactive accounts will be locked.') . '</dt>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dt>' . t('User expire settings.') . '</dt>';
$output .= '<dd>' . t('This module has a configuration page, see the <a href=":user_expire">User Expire settings</a>.', [
':user_expire' => Url::fromRoute('user_expire.admin')->toString(),
]) . '</dd>';
break;
}
return $output;
}
/**
* Implements hook_user_load().
*/
function user_expire_user_load($users): void {
foreach ($users as $uid => $user) {
$query = \Drupal::database()->select('user_expire', 'ue');
$expiration = $query->condition('ue.uid', $uid)
->fields('ue', ['expiration'])
->execute()
->fetchField();
if (!empty($expiration)) {
$user->expiration = $expiration;
}
}
}
/**
* Implements hook_user_login().
*/
function user_expire_user_login($account): void {
user_expire_notify_user();
}
/**
* Implements hook_user_cancel().
*/
function user_expire_user_cancel($edit, $account, $method): void {
user_expire_set_expiration($account);
}
/**
* Implements hook_user_delete().
*/
function user_expire_user_delete($account): void {
user_expire_set_expiration($account);
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Add the user expire form to an individual user's account page.
*
* @see \Drupal\user\ProfileForm::form()
*/
function user_expire_form_user_form_alter(&$form, FormStateInterface $form_state): void {
if (\Drupal::currentUser()->hasPermission('set user expiration')) {
$entity = $form_state->getFormObject()->getEntity();
$form['user_expire'] = [
'#type' => 'details',
'#title' => t('User expiration'),
'#open' => TRUE,
'#weight' => 5,
];
$form['user_expire']['user_expiration'] = [
'#title' => t('Set expiration for this user'),
'#type' => 'checkbox',
'#default_value' => !empty($entity->expiration),
];
$form['user_expire']['container'] = [
'#type' => 'container',
'#states' => [
'invisible' => [
':input[name="user_expiration"]' => ['checked' => FALSE],
],
],
];
$form['user_expire']['container']['user_expiration_date'] = [
'#title' => t('Expiration date'),
'#type' => 'datetime',
'#description' => t('The date on which this account will be disabled.'),
'#date_date_format' => 'Y-m-d',
'#date_time_element' => 'none',
'#default_value' => $entity->expiration ? DrupalDateTime::createFromTimestamp($entity->expiration) : NULL,
'#required' => !empty($form_state->getValue('user_expiration')),
];
}
$form['actions']['submit']['#submit'][] = 'user_expire_user_profile_form_submit';
}
/**
* Submit callback for the user profile form to save the contact page setting.
*/
function user_expire_user_profile_form_submit($form, FormStateInterface $form_state): void {
$account = $form_state->getFormObject()->getEntity();
if ($account->id() && $form_state->hasValue('user_expiration_date')) {
$account->user_expiration_date = $form_state->getValue('user_expiration_date');
$account->user_expiration = $form_state->getValue('user_expiration');
$account->expiration = $form_state->getValue('user_expiration_date');
_user_expire_save($account);
}
}
/**
* Implements hook_user_insert().
*/
function user_expire_user_insert(EntityInterface $entity): void {
_user_expire_save($entity);
}
/**
* Save expiration date from user edit form.
*
* @param object $account
* A user object to modify.
*/
function _user_expire_save(object $account): void {
if (isset($account->user_expiration) && $account->user_expiration) {
if (is_array($account->user_expiration_date) && isset($account->user_expiration_date['month'])) {
$time_for_datetime = $account->user_expiration_date['year'] . '-' . $account->user_expiration_date['month'] . '-' . $account->user_expiration_date['day'];
}
else {
$time_for_datetime = $account->user_expiration_date;
}
$new_date = new DateTime($time_for_datetime, new DateTimeZone(date_default_timezone_get()));
$new_date->setTime(0, 0, 0);
$timestamp = $new_date->getTimestamp();
user_expire_set_expiration($account, $timestamp);
}
else {
user_expire_set_expiration($account);
}
}
/**
* Implements hook_cron().
*/
function user_expire_cron(): void {
// Start with per-role warnings (if enabled).
$config = \Drupal::configFactory()->get('user_expire.settings');
$send_expiration_warnings = $config->get('send_expiration_warnings') ?? TRUE;
if ($send_expiration_warnings) {
user_expire_expire_by_role_warning();
}
// Then do per-user blocking.
user_expire_process_per_user_expiration();
// Then per-role inactivity blocking.
user_expire_expire_by_role();
}
/**
* Expires users who have an expiration that has passed.
*/
function user_expire_process_per_user_expiration(): void {
// Retrieve list of all users to be disabled.
$query = \Drupal::database()->select('user_expire', 'ue');
$expired_users = $query->condition('ue.expiration', \Drupal::time()->getRequestTime(), '<=')
->fields('ue', ['uid'])
->execute()
->fetchCol();
$accounts = [];
foreach ($expired_users as $uid) {
$accounts[] = \Drupal::entityTypeManager()->getStorage('user')->load($uid);
}
user_expire_expire_users($accounts);
}
/**
* Set a specific user's expiration time.
*
* @param object $account
* A user object to modify.
* @param int $expiration
* (Optional) An expiration time to set for the user. If this value is
* omitted, it will be used to reset a user's expiration time.
*/
function user_expire_set_expiration(object $account, ?int $expiration = NULL): void {
if (!empty($expiration)) {
// If there's an expiration, save it.
\Drupal::database()->merge('user_expire')
->key('uid', $account->id())
->fields([
'uid' => $account->id(),
'expiration' => $expiration,
])
->execute();
$account->expiration = $expiration;
user_expire_notify_user($account);
}
else {
// If the expiration is not set, delete any value that might be set.
if (!$account->isNew()) {
// New accounts can't have a record to delete.
// Existing records (!is_new) might.
// Remove user expiration times for this user.
$deleted = \Drupal::database()->delete('user_expire')
->condition('uid', $account->id())
->execute();
// Notify user that expiration time has been deleted.
if ($deleted) {
\Drupal::messenger()->addMessage(t("%name's expiration date has been reset.", ['%name' => $account->getAccountName()]));
}
}
}
}
/**
* Expire a group of users.
*
* @param array $accounts
* A set of user objects to expire.
*/
function user_expire_expire_users(array $accounts): void {
foreach ($accounts as $account) {
if ($account) {
// Block user's account.
$account->block();
\Drupal::entityTypeManager()->getStorage('user')->save($account);
// Remove current expiration time.
user_expire_set_expiration($account);
// Log notification to watchdog.
\Drupal::logger('user_expire')->info('User %name has expired.', ['%name' => $account->getAccountName()]);
}
}
}
/**
* Expire a single user.
*
* @param object $account
* A single user object to expire.
*/
function user_expire_expire_user(object $account): void {
user_expire_expire_users([$account]);
}
/**
* Displays a message to users with expiring accounts.
*
* @param object $account
* (Optional) A user object on which to report.
*/
function user_expire_notify_user(?object $account = NULL): void {
$user = \Drupal::currentUser();
if (is_null($account)) {
$account = $user;
}
// Only display a message on accounts with a current expiration date.
if (empty($account->expiration)) {
return;
}
if ($user->id() == $account->id()) {
// Notify current user that expiration time is in effect.
\Drupal::messenger()->addMessage(t("Your account's expiration date is set to @date.", ['@date' => \Drupal::service('date.formatter')->format($account->expiration)]));
}
else {
// Notify user that expiration time is in effect for this user.
\Drupal::messenger()->addMessage(t("%name's expiration date is set to @date.", [
'%name' => $account->getAccountName(),
'@date' => \Drupal::service('date.formatter')->format($account->expiration),
]));
}
}
/**
* Warns users with an upcoming expiration by roles.
*/
function user_expire_expire_by_role_warning(): void {
$config = \Drupal::configFactory()->getEditable('user_expire.settings');
$logger = \Drupal::logger('user_expire');
$last_run = \Drupal::state()->get('user_expire_last_run', 0);
$warning_frequency = $config->get('frequency');
// Warn people every 2 days.
if ($last_run && $last_run > (\Drupal::time()->getRequestTime() - $warning_frequency)) {
\Drupal::logger('user_expire')->debug('Skipping warning as it was run within the last @hours hours',
['@hours' => ($warning_frequency / (60 * 60))]);
return;
}
// Find people to warn.
$rules = user_expire_get_role_rules();
$rules = array_filter($rules);
$warning_offset = $config->get('offset');
foreach ($rules as $rid => $inactivity_period) {
$uids_to_warn = user_expire_find_users_to_expire_by_role($rid, $inactivity_period - $warning_offset);
if ($uids_to_warn) {
foreach ($uids_to_warn as $uid) {
$account = \Drupal::entityTypeManager()->getStorage('user')->load($uid->uid);
if (!$account) {
$logger->debug('Skipping warning @uid as it failed to load a valid user', [
'@uid' => $uid->uid,
]);
}
else {
// Send a notification email.
$logger->info('Sending warning about expiring account @name by role', ['@name' => $account->getAccountName()]);
\Drupal::service('plugin.manager.mail')->mail('user_expire', 'expiration_warning', $account->getEmail(), $account->getPreferredLangcode(),
[
'account' => $account,
]
);
}
}
}
}
\Drupal::state()->set('user_expire_last_run', \Drupal::time()->getRequestTime());
}
/**
* Expires user by roles according to rules in the database.
*/
function user_expire_expire_by_role(): void {
$rules = user_expire_get_role_rules();
$logger = \Drupal::logger('user_expire');
foreach ($rules as $rid => $inactivity_period) {
$uids_to_expire = user_expire_find_users_to_expire_by_role($rid, $inactivity_period);
if ($uids_to_expire) {
foreach ($uids_to_expire as $uid) {
$account = \Drupal::entityTypeManager()->getStorage('user')->load($uid->uid);
if (!$account) {
$logger->warning('Skipping @uid as it failed to load a valid user', [
'@uid' => $uid->uid,
]);
}
else {
$logger->info('Expiring account @name by role', ['@name' => $account->getAccountName()]);
user_expire_expire_user($account);
}
}
}
}
}
/**
* Finds users to expire by role and expiration period.
*
* @param string $role_id
* The role ID to search for.
* @param int $seconds_since_login
* Seconds since login. To find users *about* to expire, use a smaller number.
*
* @return \Drupal\Core\Database\StatementInterface|null
* Returns an iterator for use in a loop.
*/
function user_expire_find_users_to_expire_by_role(string $role_id, int $seconds_since_login): ?StatementInterface {
// An inactivity period of zero means the rule is disabled for the role.
if (empty($seconds_since_login)) {
return NULL;
}
// Find all the of users that need to be expired.
$query = \Drupal::database()->select('users_field_data', 'u');
$query->fields('u', ['uid'])
->condition('status', 1, '=')
->condition('u.uid', 0, '<>');
// Conditional fragment for checking on access.
$db_and_access = new Condition('AND');
$db_and_access->condition('u.access', \Drupal::time()->getRequestTime() - $seconds_since_login, '<=')
->condition('u.access', 0, '>');
// Conditional fragment for checking on created.
$db_and_created = new Condition('AND');
$db_and_created->condition('u.created', \Drupal::time()->getRequestTime() - $seconds_since_login, '<=')
->condition('u.access', 0, '=');
// Now OR the access and created fragments together.
$access_or_created = new Condition('OR');
$access_or_created->condition($db_and_access)
->condition($db_and_created);
// And finally, AND them together with the status and uid checks.
$query->condition($access_or_created);
// If this role is not the authenticated role, add a condition on the role.
// The Authenticated "role" is not in this table as it affects all users.
if (RoleInterface::AUTHENTICATED_ID != $role_id) {
$query->join('user__roles', 'ur', 'u.uid = ur.entity_id');
$query->condition('ur.roles_target_id', $role_id, '=');
}
return $query->execute();
}
/**
* Gets the role inactivity rules.
*
* @return mixed
* An array of objects keyed by rid of rid and inactivity_period or FALSE.
*/
function user_expire_get_role_rules(): mixed {
$config_factory = \Drupal::configFactory();
$config = $config_factory->get('user_expire.settings');
return array_filter($config->get('user_expire_roles')) ?: [];
}
/**
* Implements hook_mail().
*/
function user_expire_mail($key, &$message, $params): void {
if ($key == 'expiration_warning') {
$token_service = \Drupal::token();
$language_manager = \Drupal::languageManager();
$langcode = $message['langcode'];
$variables = ['user' => $params['account']];
$language = $language_manager->getLanguage($langcode);
$original_language = $language_manager->getConfigOverrideLanguage();
$language_manager->setConfigOverrideLanguage($language);
$config_factory = \Drupal::configFactory();
$config = $config_factory->get('user_expire.settings');
$token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE];
$message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($config->get('expiration_warning_mail.subject'), $variables, $token_options));
$message['body'][] = $token_service->replace($config->get('expiration_warning_mail.body'), $variables, $token_options);
$language_manager->setConfigOverrideLanguage($original_language);
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for user entities.
*
* If the account was blocked but is now active, update the expiry so it is
* not re-blocked by the next cron run.
*/
function user_expire_user_presave(User $account) {
$original = DeprecationHelper::backwardsCompatibleCall(
\Drupal::VERSION,
'11.2.0',
fn() => $account->getOriginal(),
fn() => $account->original,
);
if (!empty($original) && $original->isBlocked() && $account->isActive()) {
$account->setLastAccessTime(\Drupal::time()->getRequestTime());
}
}
