You are here

password_policy.module in Password Policy 8.3

Module file for the Password Policy module.

File

password_policy.module
View 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;
}

Functions

Namesort descending Description
password_policy_check_constraints_password_confirm_process Custom callback to update the password confirm element.
password_policy_cron Implements hook_cron().
password_policy_element_info_alter Implements hook_element_info_alter().
password_policy_form_user_form_alter Implements hook_form_FORM_ID_alter() for user_form().
password_policy_help Implements hook_help().
password_policy_load Menu argument loader. Returns a password policy entity.
password_policy_user_presave
_password_policy_check_constraints AJAX callback for user form.
_password_policy_get_edited_user_roles Gets the edited user roles for the given form.
_password_policy_show_policy Determine if the password policy should be shown.
_password_policy_user_profile_form_after_build After build callback for the user profile form.
_password_policy_user_profile_form_submit Set last password reset and expiration fields on password update.
_password_policy_user_profile_form_validate Check if password policies failed.