role_expire-8.x-1.x-dev/role_expire.module
role_expire.module
<?php
/**
* @file
* Role Expire module.
*
* Enables user roles to expire on given time.
*/
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\role_expire\Event\RoleExpiresEvent;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* Add role expiration fields to user register/edit forms.
*/
function role_expire_form_user_form_alter(&$form, FormStateInterface $form_state): void {
$account = \Drupal::routeMatch()->getParameter('user');
$form = array_merge_recursive($form, role_expire_add_expiration_input($account));
$form['#validate'][] = 'role_expire_user_form_submit_validate';
$form['actions']['submit']['#submit'][] = 'role_expire_user_form_submit';
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Add role default duration field to role edit form.
*/
function role_expire_form_user_role_form_alter(&$form, FormStateInterface $form_state): void {
if (\Drupal::currentUser()->hasPermission('edit role expire default duration') || \Drupal::currentUser()->hasPermission('administer users')) {
$formatted_link = new FormattableMarkup(
'<a href="@link" target="_blank">strtotime</a>',
[
'@link' => 'http://php.net/manual/en/function.strtotime.php',
]
);
$rid = $form['id']['#default_value'] ?? '';
$form['role_expire'] = [
'#title' => t("Default duration for the role"),
'#type' => 'textfield',
'#size' => 30,
'#default_value' => \Drupal::service('role_expire.api')
->getDefaultDuration($rid),
'#maxlength' => 32,
'#attributes' => ['class' => ['role-expire-role-expiry']],
'#description' => t('Enter the time span you want to set as the default duration for this role. Examples: 12 hours, 1 day, 3 days, 4 weeks, 3 months, 1 year. Leave blank for no default duration. (If you speak php, this value may be any @link-compatible relative form.)',
['@link' => $formatted_link]
),
];
$form['#validate'][] = 'role_expire_user_admin_role_validate';
$form['actions']['submit']['#submit'][] = 'role_expire_user_admin_role_submit';
$form['actions']['delete']['#submit'][] = 'role_expire_user_admin_role_submit_delete';
}
}
/**
* Form validation handler called by role_expire_form_user_admin_role_alter.
*
* Ensure that the specified duration is a valid, relative, positive strtotime-
* compatible string.
*/
function role_expire_user_admin_role_validate($form, FormStateInterface &$form_state): void {
$values = $form_state->getValues();
if (!empty($values['role_expire'])) {
$duration_string = Html::escape($values['role_expire']);
/*
* Make sure it's a *relative* duration string. That is, it will result in a
* different strtotime when a different 'now' value is used.
*/
$now = time();
$timestamp = strtotime($duration_string, $now);
$timestamp2 = strtotime($duration_string, $now - 100);
if ($timestamp === FALSE || $timestamp < 0) {
// Invalid format.
$form_state->setErrorByName('role_expire', 'Role expiry default duration must be a strtotime-compatible string.');
}
elseif ($timestamp < $now) {
// In the past.
$form_state->setErrorByName('role_expire', 'Role expiry default duration must be a <strong>future</strong> strtotime-compatible string.');
}
elseif ($timestamp == $timestamp2) {
/*
* This is an absolute (or special) timestamp. That's not allowed
* (not relative).
*/
$form_state->setErrorByName('role_expire', 'Role expiry default duration must be a <strong>relative</strong> strtotime-compatible string.');
}
}
}
/**
* Form submit handler called by role_expire_form_user_admin_role_alter.
*
* Updates default duration in database.
*/
function role_expire_user_admin_role_submit($form, FormStateInterface &$form_state): void {
$values = $form_state->getValues();
/*
* If the form doesn't specify a default duration, then delete default
* duration. Otherwise, set the default duration to what's specified.
*/
if (!empty($values['role_expire'])) {
$duration_string = Html::escape($values['role_expire']);
\Drupal::service('role_expire.api')->setDefaultDuration($values['id'], $duration_string);
\Drupal::service('messenger')->addMessage('New default role expiration set.');
}
else {
\Drupal::service('role_expire.api')->deleteDefaultDuration($values['id']);
}
}
/**
* Form delete handler called by role_expire_form_user_admin_role_alter.
*
* Removes default duration in database.
*/
function role_expire_user_admin_role_submit_delete($form, FormStateInterface &$form_state): void {
$values = $form_state->getValues();
\Drupal::service('role_expire.api')->deleteDefaultDuration($values['id']);
}
/**
* Form validation handler called by user_register_form & user_form alter hooks.
*
* Allows to get and save the current roles of the user before the new user data
* is actually saved. By doing this, in the submit method we can ensure role
* expire data consistency.
*
* https://drupal.stackexchange.com/questions/200620/insert-a-value-to-form-state
*/
function role_expire_user_form_submit_validate($form, FormStateInterface &$form_state): void {
$account = $form_state->getFormObject()->getEntity();
$original_roles = $account->getRoles();
$form_state->set('original_roles', $original_roles);
}
/**
* Form submit handler called by user_register_form and user_form alter hooks.
*
* @todo This method needs debugging.
*
* On D7 version, this code was inside hook_user_update. Updates default
* duration in database.
*/
function role_expire_user_form_submit($form, FormStateInterface &$form_state): void {
$values = $form_state->getValues();
/*
* Only rely on Role Delegation data if the user hasn't accessed to the normal
* roles field.
*/
if (!\Drupal::currentUser()->hasPermission('administer permissions')) {
// If Role Delegation module is used.
if (isset($values['role_change'])) {
$values['roles'] = [];
foreach ($values['role_change'] as $rid) {
if (isset($rid['target_id'])) {
$values['roles'][] = $rid['target_id'];
}
else {
$values['roles'][] = $rid;
}
}
}
}
$account = $form_state->getFormObject()->getEntity();
$original_roles = $form_state->get('original_roles') ?? [];
if ((\Drupal::currentUser()->hasPermission('edit users role expire') || \Drupal::currentUser()->hasPermission('administer users'))) {
// Add roles expiry information for the user role.
foreach ($values as $key => $value) {
if (strpos($key, 'role_expire_') === 0) {
$rid = substr($key, strlen('role_expire_'));
if ($value != '' && in_array($rid, $values['roles'])) {
$expiry_timestamp = strtotime($value);
\Drupal::service('role_expire.api')->writeRecord($account->id(), $rid, $expiry_timestamp);
}
else {
$roleExpirationCanBeDeleted = \Drupal::service('role_expire.api')->roleExpirationCanBeDeletedOnUserEditSave($rid);
if ($roleExpirationCanBeDeleted) {
\Drupal::service('role_expire.api')
->deleteRecord($account->id(), $rid, FALSE);
}
}
}
}
if (isset($values['roles'])) {
/*
* Add default expiration to any new roles that have been given to the
* user.
*/
$new_roles = array_diff($values['roles'], $original_roles);
if (!empty($new_roles)) {
/*
* We have the new roles, loop over them and see whether we need to
* assign expiry to them.
*/
foreach ($new_roles as $role_id) {
\Drupal::service('role_expire.api')->processDefaultRoleDurationForUser($role_id, $account->id());
}
}
// Remove expiration for roles that have been removed from the user.
$del_roles = array_diff($original_roles, $values['roles']);
if (!empty($del_roles)) {
/*
* We have the deleted roles, loop over them and remove their expiry
* info.
*/
foreach ($del_roles as $role_id) {
$roleExpirationCanBeDeleted = \Drupal::service('role_expire.api')->roleExpirationCanBeDeletedOnUserEditSave($role_id);
if ($roleExpirationCanBeDeleted) {
\Drupal::service('role_expire.api')
->deleteRecord($account->id(), $role_id);
}
}
}
} // if values[roles]
} // if permissions
}
/**
* Implements hook_user_insert().
*/
function role_expire_user_insert(UserInterface $account): void {
$role_expire_api = \Drupal::service('role_expire.api');
$enabled_roles = $role_expire_api->getEnabledExpirationRoles();
if (!empty($enabled_roles)) {
// This adds default expiration to any new
// roles that have been given to the user.
$roles = $account->getRoles();
// We have the new roles, loop over them and
// see whether we need to assign expiry to them.
foreach ($roles as $role_id) {
if (in_array($role_id, $enabled_roles)) {
$role_expire_api->processDefaultRoleDurationForUser($role_id, $account->id());
}
}
}
}
/**
* Implements hook_user_cancel().
*/
function role_expire_user_cancel($edit, UserInterface $account, $method): void {
// Delete user records.
\Drupal::service('role_expire.api')->deleteUserRecords($account->id());
}
/**
* Implements hook_user_delete().
*/
function role_expire_user_delete(UserInterface $account): void {
// Delete user records.
\Drupal::service('role_expire.api')->deleteUserRecords($account->id());
}
/**
* Implements hook_user_load().
*/
function role_expire_user_load($users): void {
/*
* We don't load the information to the user object. Other modules can use
* our API to query the information.
*
* Load the starter roles into a static cache so it is easy to see what has
* changed later on.
*/
foreach ($users as $account) {
_role_static_user_roles($account->id(), $account->getRoles());
}
}
/**
* Implements hook_ENTITY_TYPE_view() for user entities.
*/
function role_expire_user_view(&$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode): void {
$account = $build['#user'];
$currentUser = \Drupal::currentUser();
if ($display->getComponent('role_expire')) {
// Only show the role expire field to role administrators or the user.
if ($currentUser->hasPermission('administer role expire')
|| $currentUser->hasPermission('edit users role expire')
|| $currentUser->hasPermission('administer users')
|| $currentUser->id() == $account->id()
) {
// 1. Gather all role expiration information.
$roles = [];
$expiry_roles = \Drupal::service('role_expire.api')->getAllUserRecords($account->id());
$role_names = array_map(
fn(RoleInterface $role) => $role->label(),
Role::loadMultiple());
foreach (($account->getRoles()) as $rid) {
if (array_key_exists($rid, $expiry_roles)) {
$roles[] = t("%role role expiration date: %datetime", [
'%role' => $role_names[$rid],
'%datetime' => \Drupal::service('date.formatter')->format(
$expiry_roles[$rid]
),
]
);
}
else {
$roles[] = t("%role role (no expiration set)", [
'%role' => $role_names[$rid],
]
);
}
}
// 2. Build role expiration information.
if ($roles) {
$build['role_expire'] = [
'#theme' => 'item_list',
'#items' => $roles,
'#title' => t('Roles'),
'#attributes' => ['class' => ['role-expiry-roles']],
'#weight' => 1000,
];
}
}
}
}
/**
* Implements hook_entity_extra_field_info().
*/
function role_expire_entity_extra_field_info(): array {
// Add pseudo field.
$fields['user']['user']['display']['role_expire'] = [
'label' => t('Role expiration'),
'weight' => 1000,
];
return $fields;
}
/**
* Implements hook_cron().
*
* @todo This method needs intensive debugging.
*/
function role_expire_cron(): void {
$expires = \Drupal::service('role_expire.api')->getExpired();
if ($expires) {
foreach ($expires as $expire) {
// Remove the role expiration record from the role_expires table.
\Drupal::service('role_expire.api')->deleteRecord($expire->uid, $expire->rid);
// Remove the role from the user.
$account = User::load($expire->uid);
// If the account *does* exist, update it.
if (!empty($account)) {
// Assign a new role after expiration if requested given configuration.
$new_roles = \Drupal::service('role_expire.api')->getRolesAfterExpiration();
if (!empty($new_roles) && !empty($new_roles[$expire->rid])) {
$new_rid = $new_roles[$expire->rid];
$account->addRole($new_rid);
\Drupal::service('role_expire.api')->processDefaultRoleDurationForUser($new_rid, $account->id());
\Drupal::logger('role_expire')->notice(t('Added role @role to user @account.',
[
'@role' => $new_rid,
'@account' => $account->id(),
]
)
);
}
$account->removeRole($expire->rid);
$account->save();
/*
* Rules integration.
* #3193800: RoleExpiresEvent should not depend on Rules module
*/
$event = new RoleExpiresEvent($account, $expire->rid);
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch($event, RoleExpiresEvent::EVENT_NAME);
\Drupal::logger('role_expire')->notice(t('Removed role @role from user @account.',
[
'@role' => $expire->rid,
'@account' => $account->id(),
]
)
);
}
else {
// The account doesn't exist. Throw a warning message.
\Drupal::logger('role_expire')->notice(t('Data integrity warning: Role_expire table updated, but no user with uid @uid.', ['@uid' => $expire->uid]));
}
}
}
}
/**
* Add form element that accepts the role expiration time.
*
* @param \Drupal\user\Entity\UserInterface $account
* Edited user or null.
*
* @return array
* Form element.
*/
function role_expire_add_expiration_input(?UserInterface $account): array {
$form = [];
if (\Drupal::currentUser()->hasPermission('edit users role expire') || \Drupal::currentUser()->hasPermission('administer users')) {
$form['#attached']['library'][] = 'role_expire/role_expire';
$form['roles']['#attributes'] = ['class' => ['role-expire-roles']];
foreach (_role_expire_get_role() as $rid => $role) {
if (!is_null($account)) {
$expiry_timestamp = \Drupal::service('role_expire.api')->getUserRoleExpiryTime($account->id(), $rid);
}
else {
$expiry_timestamp = '';
}
// Describe in which ways the role expire field can be filled in. If there
// is a default duration configured for the role in question, this will be
// noted in the description.
$default_duration = \Drupal::service('role_expire.api')->getDefaultDuration($rid);
$options_list = [
'#theme' => 'item_list',
'#items' => [],
];
if ($default_duration) {
$options_list['#items']['blank'] = t('Leave the field blank. In this case, the role will expire according to the default setting, which is %default.', [
'%default' => $default_duration,
]);
}
else {
$options_list['#items']['blank'] = t('Leave the field blank. In this case, the role will never expire (since no default duration is configured for this role).');
}
$options_list['#items']['date-time'] = t('Enter a date and time in the format YYYY-MM-DD HH:MM:SS. The role will expire after this time.');
$options_list['#items']['relative-time'] = t("Use a relative time, for example: '1 day', '2 months', '1 year' or '3 years'.");
$description = t('You have the following options: @options', [
'@options' => \Drupal::service('renderer')->renderRoot($options_list),
]);
$description .= t('Note that role expiration depends on a cron job to run, so it might not expire at the exact time that is configured here.');
$expirationExpanded = \Drupal::service('role_expire.api')->expirationExpanded();
$defaultExpiration = !empty($expiry_timestamp) ? date("Y-m-d H:i:s", $expiry_timestamp) : '';
$defaultExpirationTitle = !empty($defaultExpiration) ? $defaultExpiration : t('No expiration set');
$form['role_expire_data_' . $rid] = [
'#type' => 'details',
'#title' => t('Role expiration (@current)', ['@current' => $defaultExpirationTitle]),
'#open' => $expirationExpanded ?? FALSE,
'#attributes' => ['class' => ['role-expire-details']],
];
// Display a field in which the role expiration time can be configured for
// the user that is being edited.
$form['role_expire_data_' . $rid]['role_expire_' . $rid] = [
'#title' => t("%role role expiration date/time", ['%role' => $role]),
'#type' => 'textfield',
'#default_value' => $defaultExpiration,
'#attributes' => ['class' => ['role-expire-role-expiry']],
'#description' => $description,
];
}
$form['#validate'][] = '_role_expire_validate_role_expires';
}
return $form;
}
/**
* Store user roles for this page request.
*
* Helper function.
*
* @return array|false|mixed
* Array of roles
* phpcs:disable.
*/
function _role_static_user_roles($id, $roles = ''): mixed {
// phpcs:enable.
static $user_roles = [];
if (!isset($user_roles[$id]) && is_array($roles)) {
$user_roles[$id] = $roles;
}
if (!isset($user_roles[$id])) {
return FALSE;
}
else {
return $user_roles[$id];
}
}
/**
* Get valid roles.
*
* Helper function.
*
* @return array
* Array of roles.
* phpcs:disable.
*/
function _role_expire_get_role(): array {
// phpcs:enable.
$roles_out = [];
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$enabled_roles = \Drupal::service('role_expire.api')->getEnabledExpirationRoles();
// Return in the same format as in D7 version to simplify D8 upgrade.
foreach ($roles as $role) {
if (in_array($role->id(), $enabled_roles)) {
$roles_out[$role->id()] = $role->label();
}
}
return $roles_out;
}
/**
* Form validation handler for the role expiration on the user_profile_form().
*
* Helper function.
*
* @see user_profile_form()
* phpcs:disable.
*/
function _role_expire_validate_role_expires(&$form, FormStateInterface &$form_state): void {
// phpcs:enable.
$values = $form_state->getValues();
date_default_timezone_set(date_default_timezone_get());
$time = \Drupal::time()->getRequestTime();
foreach ($values as $name => $value) {
if (strpos($name, 'role_expire_') === 0 && trim($value) != '') {
$expiry_time = strtotime($value);
if (!$expiry_time) {
$form_state->setErrorByName($name, t("Role expiry is not in correct format."));
}
if ($expiry_time <= $time) {
$form_state->setErrorByName($name, t("Role expiry must be in the future."));
}
}
}
}
