password_policy.module in Password Policy 8.3
Same filename and directory in other branches
Module file for the Password Policy module.
File
password_policy.moduleView source
<?php
/**
* @file
* Module file for the Password Policy module.
*/
use Drupal\Core\Session\AccountInterface;
use Drupal\password_policy\Entity\PasswordPolicy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\UserInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
/**
* Implements hook_form_FORM_ID_alter() for user_form().
*/
function password_policy_form_user_form_alter(&$form, FormStateInterface $form_state) {
// Hide password reset field if no access.
$account = \Drupal::currentUser();
if (!$account
->hasPermission('manage password reset')) {
$form['field_last_password_reset']['#access'] = FALSE;
$form['field_password_expiration']['#access'] = FALSE;
}
// TODO - Password editing of existing account is broken, AJAX reloads
// current password and password multiple times
// user interface changes
// TODO - Consider hiding Password Strength indicator and Password
// Recommendations.
$form['account']['roles']['#weight'] = '0';
$form['account']['mail']['#weight'] = '1';
$form['account']['name']['#weight'] = '2';
$form['account']['status']['#weight'] = '5';
$form['account']['notify']['#weight'] = '6';
$form['account']['pass']['#weight'] = '3';
// Check for specific conditions.
$show_password_policy_status = _password_policy_show_policy();
// Load form if relevant.
if ($show_password_policy_status) {
$form['account']['password_policy_status'] = [
'#title' => 'Password policies',
'#type' => 'table',
'#header' => [
t('Policy'),
t('Status'),
t('Constraint'),
],
'#empty' => t('There are no constraints for the selected user roles'),
'#weight' => '4',
'#prefix' => '<div id="password-policy-status">',
'#suffix' => '</div>',
'#rows' => \Drupal::service('password_policy.validator')
->buildPasswordPolicyConstraintsTableRows($form_state
->getValue('pass', ''), $form_state
->getFormObject()
->getEntity(), _password_policy_get_edited_user_roles($form, $form_state)),
];
// Set ajax changes.
$form['account']['roles']['#ajax'] = [
'event' => 'change',
'callback' => '_password_policy_check_constraints',
'method' => 'replace',
'wrapper' => 'password-policy-status',
];
$form['#validate'][] = '_password_policy_user_profile_form_validate';
$form['#after_build'][] = '_password_policy_user_profile_form_after_build';
}
// Add the submit handler.
$form['actions']['submit']['#submit'][] = '_password_policy_user_profile_form_submit';
}
/**
* Implements hook_element_info_alter().
*/
function password_policy_element_info_alter(array &$types) {
if (isset($types['password_confirm'])) {
$types['password_confirm']['#process'][] = 'password_policy_check_constraints_password_confirm_process';
}
}
/**
* Determine if the password policy should be shown.
*
* @return bool
* Result of whether the password policy should be shown.
*/
function _password_policy_show_policy() {
$account = \Drupal::currentUser();
$config = \Drupal::config('user.settings');
$show_password_policy_status = TRUE;
if ($account
->isAnonymous() and $config
->get('verify_mail')) {
$show_password_policy_status = FALSE;
}
return $show_password_policy_status;
}
/**
* Custom callback to update the password confirm element.
*
* @param array $element
* Form element of the password confirm form field.
* @param Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $form
* The form array.
*
* @return mixed
* Updated form field element.
*/
function password_policy_check_constraints_password_confirm_process(array $element, FormStateInterface $form_state, array $form) {
$form_object = $form_state
->getFormObject();
if (method_exists($form_object, 'getEntity') && $form_object
->getEntity() instanceof UserInterface) {
if (_password_policy_show_policy()) {
$element['pass1']['#ajax'] = [
'event' => 'change',
'callback' => '_password_policy_check_constraints',
'method' => 'replace',
'wrapper' => 'password-policy-status',
'disable-refocus' => TRUE,
];
}
}
return $element;
}
/**
* After build callback for the user profile form.
*
* Hides the password policy status when the password field is not visible. Some
* modules will hide the password field if the user is authenticated through an
* external service rather than through Drupal. In this situation the password
* policy status is not applicable.
*
* @param mixed $form
* Form definition for the user profile form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state of the user profile form.
*
* @return array
* The updated form.
*/
function _password_policy_user_profile_form_after_build($form, FormStateInterface &$form_state) {
$password_invisible = empty($form['account']['pass']) || (isset($form['account']['pass']['#access']) ? !$form['account']['pass']['#access'] : FALSE);
if ($password_invisible) {
$form['account']['password_policy_status']['#access'] = FALSE;
}
return $form;
}
/**
* Check if password policies failed.
*
* @param mixed $form
* Form definition for the user profile form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state of the user profile form.
*/
function _password_policy_user_profile_form_validate(&$form, FormStateInterface $form_state) {
// When user email verification is enabled Drupal doesn't allow setting
// password on registration. The Drupal generated password will not always
// meet the applicable policies in place. In that scenario the password
// validation should be skipped. The user will have to set a password
// after clicking the one-time login link instead.
if ($form['#form_id'] === 'user_register_form' && !isset($form['account']['pass']) && \Drupal::config('user.settings')
->get('verify_mail')) {
return;
}
$expiration = $form_state
->getValue('field_password_expiration');
if (!is_null($expiration) && $expiration['value'] === FALSE) {
$form_state
->setValue('field_password_expiration', [
'value' => 0,
]);
}
// When editing a user to change something other than the password (pass
// is empty), skip the password validation as Drupal core does.
if ($form['#form_id'] == 'user_form' && empty($form_state
->getValue('pass'))) {
return;
}
$roles = _password_policy_get_edited_user_roles($form, $form_state);
$user = $form_state
->getFormObject()
->getEntity();
if ($user
->isNew()) {
$user
->setUsername($form_state
->getValue('name', ''));
}
$valid = \Drupal::service('password_policy.validator')
->validatePassword($form_state
->getValue('pass', ''), $user, $roles);
if (!$valid) {
$form_state
->setErrorByName('pass', t('The password does not satisfy the password policies.'));
}
}
/**
* Gets the edited user roles for the given form.
*/
function _password_policy_get_edited_user_roles(&$form, FormStateInterface $form_state) {
$roles = $form_state
->getValue('roles');
if (empty($roles)) {
// Get if from $form; form state is always empty the first time.
$roles = $form['account']['roles']['#default_value'];
}
$roles = array_combine($roles, $roles);
// Add user doesn't automatically register authenticated, so lets add it.
if (empty($roles)) {
$roles = [
'authenticated' => 'authenticated',
];
}
return $roles;
}
/**
* Set last password reset and expiration fields on password update.
*/
function _password_policy_user_profile_form_submit(array &$form, FormStateInterface $form_state) {
$current_pass = $form_state
->getValue('current_pass');
$new_pass = $form_state
->getValue('pass');
// Get the uid from user object.
$user = $form_state
->getFormObject()
->getEntity();
$uid = $user
->id();
// Update if both current and new password fields are filled out. Depending
// on policy settings, user may be allowed to use same password again.
if ($uid && ($current_pass || $form_state
->get('user_pass_reset')) && $new_pass) {
$date = \Drupal::service('date.formatter')
->format(\Drupal::time()
->getRequestTime(), 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE);
$user
->set('field_last_password_reset', $date);
$user
->set('field_password_expiration', '0');
$user
->save();
}
}
/**
* {@inheritdoc}
*/
function password_policy_user_presave(EntityInterface $entity) {
if (!$entity
->id()) {
$date = \Drupal::service('date.formatter')
->format(\Drupal::time()
->getRequestTime(), 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE);
$entity
->set('field_last_password_reset', $date);
$entity
->set('field_password_expiration', '0');
}
}
/**
* AJAX callback for user form.
*/
function _password_policy_check_constraints($form, $form_state) {
return $form['account']['password_policy_status'];
}
/**
* Implements hook_cron().
*
* Looks for expired passwords and updates the expiration based on the policy
* assigned.
*/
function password_policy_cron() {
// Load each policy.
$policies = \Drupal::entityTypeManager()
->getStorage('password_policy')
->loadMultiple();
$current_time = \Drupal::time()
->getRequestTime();
/** @var \Drupal\password_policy\Entity\PasswordPolicy $policy */
foreach ($policies as $policy) {
// Check each policy configured w/ a password expiration > than 0 days.
if ($policy
->getPasswordReset() > 0) {
// Load user roles for policy.
$policy_roles = $policy
->getRoles();
if (empty($policy_roles)) {
continue;
}
// Determine date user accounts expired.
$expire_timestamp = strtotime('-' . $policy
->getPasswordReset() . ' days', $current_time);
$expire_date = \Drupal::service('date.formatter')
->format($expire_timestamp, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE);
// Configurable limit to users per policy per run, to prevent OOM errors.
$threshold = \Drupal::config('password_policy.settings')
->get('cron_threshold');
// Do not continue with User query if the policy's expire date is less
// than the install time of the module itself. This prevents the policy
// from immediately applying to all users after initial module install.
$install_time = \Drupal::state()
->get('password_policy.install_time');
if ($install_time && $install_time >= $expire_date) {
$users = [];
}
else {
// Limit to active users.
$query = \Drupal::entityQuery('user')
->condition('status', 1);
// Limit to roles set by policy configuration.
if (!in_array(AccountInterface::AUTHENTICATED_ROLE, $policy_roles)) {
$query
->condition('roles', $policy_roles, 'IN');
}
// Create condition groups for users with no value for the
// `field_password_expiration` and `field_last_password_reset` fields.
// This will be _all users_ after initial module installation.
$notset_group = $query
->andConditionGroup()
->condition('field_password_expiration', NULL, 'IS NULL')
->condition('field_last_password_reset', NULL, 'IS NULL');
// Add condition group for users with a `field_password_expiration`
// value and `field_last_password_reset` value less than or equal the
// current expire date for the policy.
$isset_group = $query
->andConditionGroup()
->condition('field_password_expiration', 0)
->condition('field_last_password_reset', $expire_date, '<=');
// Combine and add groups to query.
$combined_group = $query
->orConditionGroup()
->condition($notset_group)
->condition($isset_group);
$query
->condition($combined_group);
// Limit the number of results to the cron threshold setting.
$query
->condition('uid', 0, '>')
->range(0, $threshold);
$valid_list = $query
->execute();
// Load User Objects.
$users = \Drupal::entityTypeManager()
->getStorage('user')
->loadMultiple($valid_list);
}
// Expire passwords.
/** @var \Drupal\user\UserInterface $user */
foreach ($users as $user) {
$user
->set('field_password_expiration', '1');
$user
->save();
}
}
}
}
/**
* Menu argument loader. Returns a password policy entity.
*
* @param string $id
* ID of the password policy entity.
*
* @return \Drupal\Core\Entity\EntityInterface
* Returns a password policy object.
*/
function password_policy_load($id) {
return PasswordPolicy::load($id);
}
/**
* Implements hook_help().
*/
function password_policy_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.password_policy':
$text = file_get_contents(dirname(__FILE__) . "/README.md");
if (!\Drupal::moduleHandler()
->moduleExists('markdown')) {
return '<pre>' . $text . '</pre>';
}
else {
// Use the Markdown filter to render the README.
$filter_manager = \Drupal::service('plugin.manager.filter');
$settings = \Drupal::configFactory()
->get('markdown.settings')
->getRawData();
$config = [
'settings' => $settings,
];
$filter = $filter_manager
->createInstance('markdown', $config);
return $filter
->process($text, 'en');
}
}
return NULL;
}