simple_account_policy-1.0.0/src/AccountPolicy.php
src/AccountPolicy.php
<?php
namespace Drupal\simple_account_policy;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\simple_account_policy\Event\AccountPolicyActivateEvent;
use Drupal\simple_account_policy\Event\AccountPolicyBlockEvent;
use Drupal\simple_account_policy\Event\AccountPolicyDeleteEvent;
use Drupal\simple_account_policy\Event\AccountPolicyWarningEvent;
use Drupal\user\UserInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Account Policy.
*/
class AccountPolicy implements AccountPolicyInterface {
use StringTranslationTrait;
/**
* Event Dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $db;
/**
* Config Factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* State Interface.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Renderer Interface.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Policy validation parameters.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* AccountPolicy constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* Config Factory.
* @param \Drupal\Core\Database\Connection $database
* Connection.
* @param \Drupal\Core\State\StateInterface $state
* State Interface.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event Dispatcher interface.
* @param \Drupal\Core\Render\RendererInterface $renderer
* Renderer Interface.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
Connection $database,
StateInterface $state,
EventDispatcherInterface $event_dispatcher,
RendererInterface $renderer,
) {
$this->configFactory = $config_factory;
$this->db = $database;
$this->state = $state;
$this->eventDispatcher = $event_dispatcher;
$this->renderer = $renderer;
$this->config = $this->configFactory->getEditable('simple_account_policy.settings');
}
/**
* {@inheritdoc}
*/
public function applyPolicy(UserInterface $user): bool {
$ignore_user = FALSE;
$username_ignore_patterns = $this->config->get('username_ignore_patterns') ?? [];
if (!empty($username_ignore_patterns)) {
$pattern = '(' . implode('|', array_map('preg_quote', array_filter($username_ignore_patterns))) . ')';
$ignore_user = (preg_match("/$pattern/", $user->getAccountName(), $matches));
}
// phpcs:disable Squiz.WhiteSpace.LanguageConstructSpacing.IncorrectSingle
return
// We need to validate an existing user.
$user &&
// Don't apply if user has bypass permission.
!$user->hasPermission("bypass account policy") &&
// Don't apply if user need to be ignored (by account name).
!$ignore_user &&
// Do not deleted user anonymous
// https://www.drupal.org/project/simple_account_policy/issues/3388501.
!$user->isAnonymous();
// phpcs:enable
}
/**
* {@inheritdoc}
*/
public function validate(UserInterface $user, $data = []): array {
$errors = [];
if ($this->applyPolicy($user)) {
// Get the values to validate. Use data first if provided.
$mail = $data['mail'] ?? $user->getEmail();
$username = $data['name'] ?? $user->getAccountName();
// Check if username matches email.
$username_match_email = $this->config->get('username_match_email') ?? FALSE;
if ($username_match_email !== FALSE) {
if ($mail !== $username) {
$errors['name'][] = 'username_match_email';
}
}
// Check if username matches patterns.
$username_match_patterns = $this->config->get('username_match_patterns') ?? [];
if (!empty($username_match_patterns)) {
foreach ($username_match_patterns as $delta => $pattern) {
if (!preg_match("/$pattern/", $mail, $matches)) {
$errors['name'][] = 'username_match_pattern_' . $delta;
}
}
}
// Check if email matches patterns.
$email_match_patterns = $this->config->get('email_match_patterns') ?? [];
if (!empty($email_match_patterns)) {
foreach ($email_match_patterns as $delta => $pattern) {
if (!preg_match("/$pattern/", $mail, $matches)) {
$errors['mail'][] = 'email_match_pattern_' . $delta;
}
}
}
}
return $errors;
}
/**
* {@inheritdoc}
*/
public function policy(?UserInterface $user = NULL, array $errors = []): array {
// Make sure we have the default field keys.
$errors += [
'mail' => [],
'name' => [],
];
$policy = [];
if ($this->applyPolicy($user)) {
// Check if username changes are allowed.
$username_prevent_changes = $this->config->get('username_prevent_changes') ?? FALSE;
if ($username_prevent_changes !== FALSE) {
$policy['name'][] = [
'#markup' => $user->isNew()
? (string) $this->t("The username can only be set once.")
: (string) $this->t("The username cannot be changed."),
];
}
// Check if username matches email.
$username_match_email = $this->config->get('username_match_email') ?? FALSE;
if ($username_match_email !== FALSE) {
$policy['name'][] = [
'#wrapper_attributes' => ['class' => [in_array('username_match_email', $errors['name']) ? 'account-policy-invalid-rule marker' : 'account-policy-valid-rule']],
'#markup' => (string) $this->t("The username must match the email address."),
];
}
// Check if username matches patterns.
$username_match_patterns = array_filter($this->config->get('username_match_patterns') ?? []);
if (!empty($username_match_patterns)) {
foreach ($username_match_patterns as $delta => $pattern) {
$policy['name'][] = [
'#wrapper_attributes' => ['class' => [in_array('username_match_pattern_' . $delta, $errors['name']) ? 'account-policy-invalid-rule marker' : 'account-policy-valid-rule']],
'#markup' => (string) $this->patternToMessage($pattern),
];
}
}
// Check if email matches patterns.
$email_match_patterns = array_filter($this->config->get('email_match_patterns') ?? []);
if (!empty($email_match_patterns)) {
foreach ($email_match_patterns as $delta => $pattern) {
$policy['mail'][] = [
'#wrapper_attributes' => ['class' => [in_array('email_match_pattern_' . $delta, $errors['name']) ? 'account-policy-invalid-rule marker' : 'account-policy-valid-rule']],
'#markup' => (string) $this->patternToMessage($pattern),
];
}
}
if (!empty($policy['name'])) {
$elements = [
'title' => [
'#markup' => (string) $this->formatPlural(count($policy['name']),
"The username must satisfy the following account policy rule:",
"The username must satisfy the following account policy rules:"
),
],
'rules' => [
'#theme' => 'item_list',
'#list_type' => 'ul',
'#items' => $policy['name'],
],
];
$policy['name'] = $this->renderer->renderPlain($elements);
}
if (!empty($policy['mail'])) {
$elements = [
'title' => [
'#markup' => (string) $this->formatPlural(count($policy['mail']),
"The email must satisfy the following account policy rule:",
"The email must satisfy the following account policy rules:"
),
],
'rules' => [
'#theme' => 'item_list',
'#list_type' => 'ul',
'#items' => $policy['mail'],
],
];
$policy['mail'] = $this->renderer->renderPlain($elements);
}
}
else {
$policy = [
'mail' => $this->t("This user bypasses the account policy."),
'name' => $this->t("This user bypasses the account policy."),
];
}
return $policy;
}
/**
* {@inheritdoc}
*/
public function getBlockTime(UserInterface $user): int {
$block_time = 0;
$inactive_period = (string) $this->config->get('inactive_period') ?? '';
if (!empty($inactive_period)) {
$lastAccessedTime = $user->getLastAccessedTime();
if ($lastAccessedTime === '0') {
$lastAccessedTime = $user->getCreatedTime();
}
if (ctype_digit($inactive_period)) {
$block_time = $lastAccessedTime + intval($inactive_period);
}
else {
$block_time = time() + ($lastAccessedTime - strtotime('-' . $inactive_period));
}
}
return $block_time;
}
/**
* {@inheritdoc}
*/
public function getWarningTime(UserInterface $user): int {
$warning_time = 0;
$block_time = $this->getBlockTime($user);
if ($block_time) {
$inactive_warning = (string) $this->config->get('inactive_warning') ?? '';
if (!empty($inactive_warning)) {
if (ctype_digit($inactive_warning)) {
$warning_time = $block_time - intval($inactive_warning);
}
else {
$warning_time = time() + ($block_time - strtotime('+' . $inactive_warning));
}
}
}
return $warning_time;
}
/**
* {@inheritdoc}
*/
public function inactiveInterval(): int {
return $this->config->get('inactive_interval') ?? 86400;
}
/**
* {@inheritdoc}
*/
public function getDeleteAfterTime(): int {
$delete_after_time = (string) $this->config->get('delete_after_time') ?? 0;
if (!empty($delete_after_time)) {
if (ctype_digit($delete_after_time)) {
$delete_after_time = time() - intval($delete_after_time);
}
else {
$delete_after_time = strtotime("$delete_after_time ago") ?: 0;
}
}
return $delete_after_time;
}
/**
* {@inheritdoc}
*/
public function shouldIssueWarning(UserInterface $user): bool {
$warning_time = $this->getWarningTime($user);
return ($warning_time && !$this->warningIssued($user) && time() > $warning_time);
}
/**
* {@inheritdoc}
*/
public function warningIssued(UserInterface $user): bool {
$warned_users = $this->state->get('simple_account_policy.warned_users', []);
return array_key_exists($user->id(), $warned_users);
}
/**
* {@inheritdoc}
*/
public function issueWarning(UserInterface $user): void {
// Keep track of issued warnings.
$warned_users = $this->state->get('simple_account_policy.warned_users', []);
$warned_users[$user->id()] = $user->id();
$this->state->set('simple_account_policy.warned_users', $warned_users);
// Send out an event so others can react to this as well.
$event = new AccountPolicyWarningEvent($user, $this);
$this->eventDispatcher->dispatch($event, AccountPolicyWarningEvent::EVENT_NAME);
}
/**
* {@inheritdoc}
*/
public function isInactive(UserInterface $user): bool {
$block_time = $this->getBlockTime($user);
return ($block_time && $user->isActive() && time() > $block_time);
}
/**
* {@inheritdoc}
*/
public function block(UserInterface $user): void {
$event = new AccountPolicyBlockEvent($user, $this);
$this->eventDispatcher->dispatch($event, AccountPolicyBlockEvent::EVENT_NAME);
}
/**
* {@inheritdoc}
*/
public function activate(UserInterface $user): void {
// Keep track of issued warnings.
$warned_users = $this->state->get('simple_account_policy.warned_users', []);
unset($warned_users[$user->id()]);
$this->state->set('simple_account_policy.warned_users', $warned_users);
// Send out an event so others can react to this as well.
$event = new AccountPolicyActivateEvent($user, $this);
$this->eventDispatcher->dispatch($event, AccountPolicyActivateEvent::EVENT_NAME);
}
/**
* Pretty print a regular expression pattern.
*
* @param array $pattern
* Pattern.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* Return Statement.
*/
protected function patternToMessage(string $pattern): string {
$beginsWithApostrophe = ($pattern[0] == '^');
$endsWithDollar = ($pattern[strlen($pattern) - 1] == '$');
if ($beginsWithApostrophe && $endsWithDollar) {
$message = $this->t("Must match %pattern.", ['%pattern' => trim($pattern, "^$")]);
}
else {
if ($beginsWithApostrophe) {
$message = $this->t("Must begin with %pattern.", ['%pattern' => ltrim($pattern, "^")]);
}
elseif ($endsWithDollar) {
$message = $this->t("Must end with %pattern.", ['%pattern' => rtrim($pattern, "$")]);
}
else {
$message = $this->t("Must contain %pattern.", ['%pattern' => $pattern]);
}
}
return $message;
}
/**
* {@inheritdoc}
*/
public function shouldBeDeleted(UserInterface $user): bool {
$lastLoginTime = $user->getLastAccessedTime() ?: $user->getCreatedTime();
$deleteAfterTime = $this->getDeleteAfterTime();
if (empty($deleteAfterTime)) {
return FALSE;
}
if ($lastLoginTime < $deleteAfterTime) {
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function delete(UserInterface $user): void {
$cancelMethod = $this->config->get('user_cancel_method');
if (empty($cancelMethod)) {
$cancelMethod = 'user_cancel_reassign';
}
$event = new AccountPolicyDeleteEvent($user, $this, $cancelMethod);
$this->eventDispatcher->dispatch($event, AccountPolicyDeleteEvent::EVENT_NAME);
}
/**
* Get the policy configuration.
*
* @return \Drupal\Core\Config\Config
* The policy configuration object.
*/
public function getConfiguration(): Config {
return $this->config;
}
}
