diff --git a/src/Annotation/UserRestrictionType.php b/src/Annotation/UserRestrictionType.php new file mode 100644 index 0000000..02ac371 --- /dev/null +++ b/src/Annotation/UserRestrictionType.php @@ -0,0 +1,37 @@ +name; + } + + /** + * {@inheritdoc} + */ + public function getExpiry() { + return $this->expiry; + } + + /** + * {@inheritdoc} + */ + public function getPattern() { + return $this->pattern; + } + + /** + * {@inheritdoc} + */ + public function getAccessType() { + return $this->access_type; + } + + /** + * {@inheritdoc} + */ + public function getRuleType() { + return $this->rule_type; + } - // Use the epoch as the default date for non-expiring rules. - // @TODO do we instead want to make the expiration date extremely far in - // the future or have another flag that signifies permanent? - const NO_EXPIRY = 0; - - const BLACKLIST = 0; - - const WHITELIST = 1; - - /** - * The name of the image style. - * - * @var string - */ - protected $name; - - protected $id; - - protected $pattern; - - /** - * The user restriction label. - * - * @var string - */ - protected $label; - - protected $access_type; - - protected $expiry; - - protected $rule_type; - - /** - * {@inheritdoc} - */ - public function id() { - return $this->name; - } - - public function getExpiry() { - return $this->expiry; - } - - public function getPattern() { - return $this->pattern; - } - - public function getAccessType() { - return $this->access_type; - } - - public function getRuleType() { - return $this->rule_type; - } } diff --git a/src/Form/UserRestrictionsAddForm.php b/src/Form/UserRestrictionsAddForm.php index 90f3c00..37479e7 100644 --- a/src/Form/UserRestrictionsAddForm.php +++ b/src/Form/UserRestrictionsAddForm.php @@ -9,22 +9,22 @@ use Drupal\Core\Form\FormStateInterface; */ class UserRestrictionsAddForm extends UserRestrictionsFormBase { - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - parent::submitForm($form, $form_state); - drupal_set_message($this->t('User restriction was created for %label.', array('%label' => $this->entity->label()))); - } + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + drupal_set_message($this->t('User restriction was created for %label.', array('%label' => $this->entity->label()))); + } - /** - * {@inheritdoc} - */ - public function actions(array $form, FormStateInterface $form_state) { - $actions = parent::actions($form, $form_state); - $actions['submit']['#value'] = $this->t('Create new user restriction'); + /** + * {@inheritdoc} + */ + public function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Create new user restriction'); - return $actions; - } + return $actions; + } } diff --git a/src/Form/UserRestrictionsFormBase.php b/src/Form/UserRestrictionsFormBase.php index 5a03f59..4094e99 100644 --- a/src/Form/UserRestrictionsFormBase.php +++ b/src/Form/UserRestrictionsFormBase.php @@ -4,138 +4,143 @@ namespace Drupal\user_restrictions\Form; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityForm; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Url; use Drupal\user_restrictions\Entity\UserRestrictions; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base form for image style add and edit forms. */ abstract class UserRestrictionsFormBase extends EntityForm { - /** - * The entity being used by this form. - * - * @var \Drupal\user_restrictions\Entity\UserRestrictions - */ - protected $entity; - - /** - * {@inheritdoc} - */ - public function form(array $form, FormStateInterface $form_state) { - - $form['label'] = array( - '#type' => 'textfield', - '#title' => $this->t('User restriction name'), - '#default_value' => $this->entity->label(), - '#required' => TRUE, - ); - $form['name'] = array( - '#type' => 'machine_name', - '#machine_name' => array( - 'exists' => ['\Drupal\user_restrictions\Entity\UserRestriction', 'load'], - ), - '#default_value' => $this->entity->id(), - '#required' => TRUE, - ); - - $form['pattern'] = array( - '#type' => 'textfield', - '#title' => $this->t('Pattern'), - '#size' => 10, - '#maxlength' => 64, - '#default_value' => $this->entity->getPattern(), - '#field_prefix' => '/', - '#field_suffix' => '/i', - '#description' => $this->t('Add a pattern for this rule to match.
Regular expressions are accepted and can be used for more complex restrictions.'), - '#required' => TRUE, - ); - - $form['access_type'] = array( - '#type' => 'radios', - '#title' => t('Access type'), - '#default_value' => $this->entity->getAccessType(), - '#options' => array(UserRestrictions::BLACKLIST => $this->t('Blacklist'), UserRestrictions::WHITELIST => $this->t('Whitelist')), - '#required' => TRUE, - ); - - $form['rule_type'] = array( - '#type' => 'radios', - '#title' => t('Restriction type'), - '#default_value' => $this->entity->getRuleType(), - '#options' => array('name' => $this->t('Username'), 'mail' => $this->t('Email')), - '#required' => TRUE, - ); - - $form['expiration'] = array( - '#type' => 'details', - '#title' => $this->t('Expiration'), - '#description' => $this->t('Set a time for this user restriction to expire or create a permanent restriction.'), - '#open' => TRUE, - '#required' => TRUE, - - ); - $form['expiration']['permanent'] = array( - '#type' => 'checkbox', - '#title' => $this->t('Never expire'), - ); - $form['expiration']['expiry_container'] = array( - '#type' => 'container', - '#states' => array( - // Hide the additional settings when the blocked email is disabled. - 'invisible' => array( - 'input[name="permanent"]' => array('checked' => TRUE), - ), - ), - '#open' => TRUE, - ); - - // Set the default expiration to be 7 days in the future. - $default_expiration = new DrupalDateTime('now +7 days'); - - if ($default_expiration = (int) $this->entity->getExpiry()) { - $default_expiration = DrupalDateTime::createFromTimestamp($default_expiration); - } - - $form['expiration']['expiry_container']['expiry'] = array( - '#type' => 'datetime', - '#default_value' => $default_expiration, - '#title_display' => 'invisible', - '#title' => $this->t('Expiry time'), - '#required' => TRUE, - ); - - return parent::form($form, $form_state); + /** + * The entity being used by this form. + * + * @var \Drupal\user_restrictions\Entity\UserRestrictions + */ + protected $entity; + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + /** @var \Drupal\user_restrictions\UserRestrictionTypeManagerInterface $type_manager */ + $type_manager = \Drupal::service('user_restrictions.type_manager'); + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('User restriction name'), + '#default_value' => $this->entity->label(), + '#required' => TRUE, + ]; + $form['name'] = [ + '#type' => 'machine_name', + '#machine_name' => [ + 'exists' => ['\Drupal\user_restrictions\Entity\UserRestriction', 'load'], + ], + '#default_value' => $this->entity->id(), + '#required' => TRUE, + ]; + + $form['pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Pattern'), + '#size' => 10, + '#maxlength' => 64, + '#default_value' => $this->entity->getPattern(), + '#field_prefix' => '/', + '#field_suffix' => '/i', + '#description' => $this->t('Add a pattern for this rule to match.
Regular expressions are accepted and can be used for more complex restrictions.'), + '#required' => TRUE, + ]; + + $form['access_type'] = [ + '#type' => 'radios', + '#title' => t('Access type'), + '#default_value' => $this->entity->getAccessType(), + '#options' => [UserRestrictions::BLACKLIST => $this->t('Blacklist'), UserRestrictions::WHITELIST => $this->t('Whitelist')], + '#required' => TRUE, + ]; + + $form['rule_type'] = [ + '#type' => 'radios', + '#title' => t('Restriction type'), + '#default_value' => $this->entity->getRuleType(), + '#options' => $type_manager->getTypesAsOptions(), + '#required' => TRUE, + ]; + + $form['expiration'] = [ + '#type' => 'details', + '#title' => $this->t('Expiration'), + '#description' => $this->t('Set a time for this user restriction to expire or create a permanent restriction.'), + '#open' => TRUE, + '#required' => TRUE, + ]; + $form['expiration']['permanent'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Never expire'), + '#default_value' => ($this->entity->getExpiry() == UserRestrictions::NO_EXPIRY), + ]; + $form['expiration']['expiry_container'] = [ + '#type' => 'container', + '#states' => [ + // Hide the additional settings when the blocked email is disabled. + 'invisible' => [ + 'input[name="permanent"]' => ['checked' => TRUE], + ], + ], + '#open' => TRUE, + ]; + + // Set the default expiration to be 7 days in the future. + $default_expiration = new DrupalDateTime('now +7 days'); + + if ($expiration = (int) $this->entity->getExpiry()) { + $default_expiration = DrupalDateTime::createFromTimestamp($expiration); } - - public function validateForm(array &$form, FormStateInterface $form_state) - { - // Store the expiration time as unixtime as configuration entities may - // only use scalar values. - - /* @var $datetime DrupalDateTime */ - $datetime = $form_state->getValue('expiry'); + $form['expiration']['expiry_container']['expiry'] = [ + '#type' => 'datetime', + '#default_value' => $default_expiration, + '#title_display' => 'invisible', + '#title' => $this->t('Expiry time'), + '#required' => TRUE, + ]; + + return parent::form($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Check for duplicate pattern. + $existing = $this->entityTypeManager->getStorage('user_restrictions')->loadByProperties(['rule_type' => $form_state->getValue('rule_type'), 'pattern' => $form_state->getValue('pattern')]); + if (!empty($existing)) { + $form_state->setError($form['pattern'], $this->t('A rule with the same pattern already exists.')); + } - if ($form_state->getValue('permanent')) { - $form_state->setValue('expiry', UserRestrictions::NO_EXPIRY); - } - else { - $form_state->setValue('expiry', (int) $datetime->format('U')); - } + // Store the expiration time as unixtime as configuration entities may + // only use scalar values. + /* @var $datetime DrupalDateTime */ + $datetime = $form_state->getValue('expiry'); - parent::validateForm($form, $form_state); + if ($form_state->getValue('permanent')) { + $form_state->setValue('expiry', UserRestrictions::NO_EXPIRY); } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - parent::save($form, $form_state); - //$form_state->setRedirectUrl(Url::fromRoute('user_restrictions.collection')); + else { + $form_state->setValue('expiry', (int) $datetime->format('U')); } -} \ No newline at end of file + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + parent::save($form, $form_state); + //$form_state->setRedirectUrl(Url::fromRoute('user_restrictions.collection')); + } + +} diff --git a/src/Plugin/UserRestrictionType/ClientIp.php b/src/Plugin/UserRestrictionType/ClientIp.php new file mode 100644 index 0000000..fa4629a --- /dev/null +++ b/src/Plugin/UserRestrictionType/ClientIp.php @@ -0,0 +1,62 @@ +requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition, $container->get('entity_type.manager'), $container->get('request_stack'), $container->get('logger.channel.user_restrictions')); + } + + /** + * {@inheritdoc} + */ + public function matches($data) { + $client_ip = $this->requestStack->getCurrentRequest()->getClientIp(); + $restriction = parent::matchesValue($client_ip); + if ($restriction) { + $this->logger->notice('Restricted client IP %client_ip matching %restriction has been blocked.', ['%client_ip' => $client_ip, '%restriction' => $restriction->link($restriction->label())]); + } + return $restriction; + } + + /** + * {@inheritdoc} + */ + public function getErrorMessage() { + return $this->t('Accessing the site from the IP %value is not allowed.', ['%value' => $this->requestStack->getCurrentRequest()->getClientIp()]); + } + +} diff --git a/src/Plugin/UserRestrictionType/Email.php b/src/Plugin/UserRestrictionType/Email.php new file mode 100644 index 0000000..7936f2d --- /dev/null +++ b/src/Plugin/UserRestrictionType/Email.php @@ -0,0 +1,42 @@ +mail = $data['mail']; + $restriction = parent::matchesValue($this->mail); + if ($restriction) { + $this->logger->notice('Restricted email %mail matching %restriction has been blocked.', ['%mail' => $this->mail, '%restriction' => $restriction->link($restriction->label())]); + } + return $restriction; + } + + /** + * {@inheritdoc} + */ + public function getErrorMessage() { + return $this->t('The email %mail is reserved, and cannot be used.', ['%mail' => $this->mail]); + } + +} diff --git a/src/Plugin/UserRestrictionType/Name.php b/src/Plugin/UserRestrictionType/Name.php new file mode 100644 index 0000000..2a64da7 --- /dev/null +++ b/src/Plugin/UserRestrictionType/Name.php @@ -0,0 +1,42 @@ +name = $data['name']; + $restriction = parent::matchesValue($this->name); + if ($restriction) { + $this->logger->notice('Restricted name %name matching %restriction has been blocked.', ['%name' => $this->name, '%restriction' => $restriction->link($restriction->label())]); + } + return $restriction; + } + + /** + * {@inheritdoc} + */ + public function getErrorMessage() { + return $this->t('The name %name is reserved, and cannot be used.', ['%name' => $this->name]); + } + +} diff --git a/src/Plugin/UserRestrictionType/UserRestrictionTypeBase.php b/src/Plugin/UserRestrictionType/UserRestrictionTypeBase.php new file mode 100644 index 0000000..cd655ab --- /dev/null +++ b/src/Plugin/UserRestrictionType/UserRestrictionTypeBase.php @@ -0,0 +1,128 @@ +entityStorage = $entity_manager->getStorage('user_restrictions'); + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition, $container->get('entity_type.manager'), $container->get('logger.channel.user_restrictions')); + } + + /** + * Check if the specified value matches the restriction. + * + * @param string $value + * String to check against all restrictions of the type. + * + * @return boolean|\Drupal\user_restrictions\Entity\UserRestrictions + * The restriction entity if the value matches one of the restrictions, + * FALSE otherwise. + */ + protected function matchesValue($value) { + // Load rules with exact pattern matches. + $exact_rules = $this->entityStorage + ->loadByProperties(['rule_type' => $this->getPluginId(), 'pattern' => $value]); + if (!empty($exact_rules)) { + // Simply take the first matching rule as we have no weight (yet). + /** @var \Drupal\user_restrictions\Entity\UserRestrictions $rule */ + $rule = reset($exact_rules); + return ($rule->getAccessType() == UserRestrictions::BLACKLIST) ? $rule : FALSE; + } + + // Load all rules of the restriction type. + $rules = $this->entityStorage + ->loadByProperties(['rule_type' => $this->getPluginId()]); + if (empty($rules)) { + return FALSE; + } + + /** @var \Drupal\user_restrictions\Entity\UserRestrictions $rule */ + foreach ($rules as $rule) { + $pattern = strtr(preg_quote($rule->getPattern()), ['%' => '.*']); + if (preg_match('/' . $pattern . '/i', $value)) { + // Exit loop after first matching pattern. + return ($rule->getAccessType() == UserRestrictions::BLACKLIST) ? $rule : FALSE; + } + } + + // Fallback to no match. + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getPatterns() { + if (!empty($this->patterns)) { + return $this->patterns; + } + $rules = $this->entityStorage + ->loadByProperties(['rule_type' => $this->getPluginId()]); + if (empty($rules)) { + return []; + } + /** @var \Drupal\user_restrictions\Entity\UserRestrictions $rule */ + foreach ($rules as $id => $rule) { + $this->patterns[$id] = $rule->getPattern(); + } + return $this->patterns; + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + return $this->pluginDefinition['label']; + } + + /** + * {@inheritdoc} + */ + public function getErrorMessage() { + return $this->t('Using reserved data.'); + } + +} diff --git a/src/Plugin/UserRestrictionTypeInterface.php b/src/Plugin/UserRestrictionTypeInterface.php new file mode 100644 index 0000000..0f3ab46 --- /dev/null +++ b/src/Plugin/UserRestrictionTypeInterface.php @@ -0,0 +1,47 @@ + 'User Restrictions Basic test', - 'description' => 'Tests creation and loading of restrictions', - 'group' => 'User Restrictions', - ); - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->name = $this->randomName(); - $this->type = 'name'; - - $restriction = entity_create('user_restrictions', array( - 'mask' => $this->name, - 'type' => $this->type, - 'status' => 1, - )); - $restriction->save(); - - $this->id = $restriction->id(); - } - - /** - * Ensure the restriction exists in the database - */ - protected function testUserRestrictionsRecordExists() { - $restriction = user_restrictions_load($this->id, TRUE); - $this->assertTrue($restriction, 'User Restriction exists in the database'); - $this->assertEqual($restriction->label(), $this->name, 'User Restriction name matches'); - $this->assertEqual($restriction->getType(), $this->type, 'User Restriction type matches'); - } - -} diff --git a/src/Tests/UserRestrictionsExpireTest.php b/src/Tests/UserRestrictionsExpireTest.php deleted file mode 100644 index cee95a2..0000000 --- a/src/Tests/UserRestrictionsExpireTest.php +++ /dev/null @@ -1,72 +0,0 @@ - 'User Restrictions Expire test', - 'description' => 'Tests expired user restrictions rules do not take effect and are deleted on cron.', - 'group' => 'User Restrictions', - ); - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Create a restriction with an expiration date in the past. - $this->name = $this->randomName(); - - $restriction = entity_create('user_restrictions', array( - 'mask' => $this->name, - 'type' => $this->mail, - 'status' => 1, - 'expire' => 1000, - )); - $restriction->save(); - - $this->id = $restriction->id(); - } - - /** - * Ensure the restriction exists in the database - */ - protected function testUserRestrictionsRecordExists() { - $restriction = user_restrictions_load($this->id, TRUE); - $this->assertTrue($restriction, 'User Restriction exists in the database'); - $this->assertEqual($restriction->label(), $this->name, 'User Restriction name matches'); - } - - /** - * Ensure an expired user may now log in. - */ - protected function testUserRestrictionsExpiredLogin() { - $account = $this->drupalCreateUser(array(), $this->name); - $this->drupalLogin($account); - } - - /** - * Ensure an expired restriction gets deleted on cron - */ - protected function testUserRestrictionsExpiredCron() { - \Drupal::service('cron')->run(); - $this->assertFalse(user_restrictions_load($this->id, TRUE), 'User Restriction was removed from the database.'); - } - -} diff --git a/src/Tests/UserRestrictionsTestBase.php b/src/Tests/UserRestrictionsTestBase.php deleted file mode 100644 index 71d4df8..0000000 --- a/src/Tests/UserRestrictionsTestBase.php +++ /dev/null @@ -1,71 +0,0 @@ -set('register', USER_REGISTER_VISITORS)->save(); - } - - /** - * Create some user restrictions - */ - protected function createRestrictions() { - - // Block any user with a name starting 'lol' - $restrictions[] = array( - 'mask' => 'lol%', - 'type' => 'name', - 'status' => 0, - ); - - // Allow the user named 'lolcats' - $restrictions[] = array( - 'mask' => 'lolcats', - 'type' => 'name', - 'status' => 1, - ); - - // Block any user with a .ru email address - $restrictions[] = array( - 'mask' => '%@%.ru', - 'type' => 'mail', - 'status' => 0, - ); - - // Specically allow typhonius@mail.ru - $restrictions[] = array( - 'mask' => 'typhonius@mail.ru', - 'type' => 'mail', - 'status' => 1, - ); - - foreach ($restrictions as $restriction) { - $entity = entity_create('user_restrictions', $restriction); - $entity->save(); - } - } - -} diff --git a/src/UserRestrictionTypeManager.php b/src/UserRestrictionTypeManager.php new file mode 100644 index 0000000..69ae504 --- /dev/null +++ b/src/UserRestrictionTypeManager.php @@ -0,0 +1,77 @@ +alterInfo('user_restriction_type_info'); + $this->setCacheBackend($cache_backend, 'user_restriction_type_plugins'); + } + + /** + * {@inheritdoc} + */ + public function getTypes() { + $instances = &drupal_static(__FUNCTION__, []); + if (empty($instances)) { + // Get registered plugins. + $plugins = $this->getDefinitions(); + // Sort plugins by weight. + uasort($plugins, array('Drupal\Component\Utility\SortArray', 'sortByWeightElement')); + foreach ($plugins as $plugin_id => $plugin) { + // Instanciate the plugin. + $instances[$plugin_id] = $this->createInstance($plugin_id, $plugin); + } + } + + return $instances; + } + + /** + * {@inheritdoc} + */ + public function getType($id) { + $instances = $this->getTypes(); + return $instances[$id]; + } + + /** + * {@inheritdoc} + */ + public function getTypesAsOptions() { + $options = []; + + foreach ($this->getTypes() as $plugin_id => $type) { + $options[$plugin_id] = $type->getLabel(); + } + + return $options; + } + +} diff --git a/src/UserRestrictionTypeManagerInterface.php b/src/UserRestrictionTypeManagerInterface.php new file mode 100644 index 0000000..6daecb0 --- /dev/null +++ b/src/UserRestrictionTypeManagerInterface.php @@ -0,0 +1,37 @@ +t('User restriction'); - $header['rule_type'] = $this->t('Rule type'); - $header['pattern'] = $this->t('Pattern'); - $header['access_type'] = $this->t('Access type'); - $header['expiry'] = $this->t('Expiry'); - return $header + parent::buildHeader(); - } - - /** - * {@inheritdoc} - */ - public function buildRow(UserRestrictions $entity) { - - $row['label'] = $entity->label(); - $row['type'] = $entity->getRuleType(); - $row['pattern'] = $entity->getPattern(); - $row['access_type'] = $entity->getAccessType() ? $this->t('Whitelisted') : $this->t('Blacklisted'); - $row['expiry'] = $entity->getExpiry() ? date('Y-m-d H:i:s', $entity->getExpiry()) : $this->t('Never'); - return $row + parent::buildRow($entity); - } - - /** - * {@inheritdoc} - */ - public function render() { - $build = parent::render(); - $build['table']['#empty'] = $this->t('There are currently no user restrictions. Add a new one.', [ - ':url' => Url::fromRoute('user_restrictions.add')->toString(), - ]); - return $build; - } + /** + * The user restriction type manager. + * + * @var \Drupal\user_restrictions\UserRestrictionTypeManagerInterface + */ + protected $typeManager; + + /** + * {@inheritdoc} + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage) { + parent::__construct($entity_type, $storage); + $this->typeManager = \Drupal::service('user_restrictions.type_manager'); + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = $this->t('User restriction'); + $header['rule_type'] = $this->t('Rule type'); + $header['pattern'] = $this->t('Pattern'); + $header['access_type'] = $this->t('Access type'); + $header['expiry'] = $this->t('Expiry'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(UserRestrictions $entity) { + $row['label'] = $entity->label(); + $row['type'] = $this->typeManager->getType($entity->getRuleType())->getLabel(); + $row['pattern'] = $entity->getPattern(); + $row['access_type'] = $entity->getAccessType() ? $this->t('Whitelisted') : $this->t('Blacklisted'); + $row['expiry'] = $entity->getExpiry() == UserRestrictions::NO_EXPIRY ? $this->t('Never') : date('Y-m-d H:i:s', $entity->getExpiry()); + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['table']['#empty'] = $this->t('There are currently no user restrictions. Add a new one.', [ + ':url' => Url::fromRoute('user_restrictions.add')->toString(), + ]); + return $build; + } } diff --git a/src/UserRestrictionsManager.php b/src/UserRestrictionsManager.php index 25b3ec5..b0c9560 100644 --- a/src/UserRestrictionsManager.php +++ b/src/UserRestrictionsManager.php @@ -1,102 +1,116 @@ getQuery() - ->sort('name') - ->execute(); - - $this->rules = $entity_storage->loadMultiple($ids); - } - - public function setName($name) { - $this->name = $name; + /** + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * The entity storage. + * @param \Drupal\user_restrictions\UserRestrictionTypeManagerInterface $type_manager + * The user restriction type manager. + * @param LoggerInterface $logger + * The user_restrictions logger channel. + */ + public function __construct(EntityTypeManagerInterface $entity_manager, UserRestrictionTypeManagerInterface $type_manager, LoggerInterface $logger) { + $this->entityStorage = $entity_manager->getStorage('user_restrictions'); + $this->typeManager = $type_manager; + $this->logger = $logger; } - public function setMail($mail) { - $this->mail = $mail; - } - - public function isRestricted() { - foreach ($this->rules as $rule) { - /* @var $rule UserRestrictions */ - - // @TODO preg_quote - - switch ($rule->getRuleType()) { - case 'name': - if (preg_match($rule->getPattern(), $this->name)) { - $this->setError('name', t('The name %name is reserved, and cannot be used.', array('%name' => $this->name))); - \Drupal::logger('user_restrictions')->notice('Restricted name %name matching %rule has been blocked.', array('%name' => $this->name, '%rule' => $rule->link($rule->label()))); - return TRUE; - } - break; - - case 'mail': - if (preg_match($rule->getPattern(), $this->mail)) { - $this->setError('mail', t('The email address %email is reserved, and cannot be used.', array('%email' => $this->mail))); - \Drupal::logger('user_restrictions')->notice('Restricted email %mail matching %rule has been blocked.', array('%mail' => $this->mail, '%rule' => $rule->link($rule->label()))); - return TRUE; - } - break; - - default: - // @TODO log this or remove it? - continue; - + /** + * {@inheritdoc} + */ + public function matchesRestrictions($data) { + /** @var \Drupal\user_restrictions\Plugin\UserRestrictionTypeInterface $type */ + foreach ($this->typeManager->getTypes() as $key => $type) { + if ($type->matches($data)) { + $this->setError($key, $type->getErrorMessage()); + // Break after first match. + return TRUE; } } + // No restrictions match. return FALSE; } + /** + * Set error message for a specific restriction type. + * + * @param string $type + * Type of restriction, i.e. "name". + * @param string $message + * Error message. + * + * @return \Drupal\user_restrictions\UserRestrictionsManagerInterface + * The service for chaining. + */ protected function setError($type, $message) { $this->errors[$type] = $message; + return $this; } + /** + * {@inheritdoc} + */ public function getErrors() { return $this->errors; } + /** + * {@inheritdoc} + */ public function deleteExpiredRules() { - foreach ($this->rules as $rule) { - /* @var $rule UserRestrictions */ - + $rules = $this->entityStorage->loadMultiple(); + /* @var $rule \Drupal\user_restrictions\Entity\UserRestrictions */ + foreach ($rules as $rule) { $expiry = $rule->getExpiry(); if ($expiry !== UserRestrictions::NO_EXPIRY && $expiry < REQUEST_TIME) { $rule->delete(); - \Drupal::logger('user_restrictions')->notice('Expired rule %label has been deleted.', array('%label' => $rule->label())); + $this->logger->notice('Expired rule %label has been deleted.', ['%label' => $rule->label()]); } } + return $this; } -} \ No newline at end of file +} diff --git a/src/UserRestrictionsManagerInterface.php b/src/UserRestrictionsManagerInterface.php index 06c1b08..2fd5986 100644 --- a/src/UserRestrictionsManagerInterface.php +++ b/src/UserRestrictionsManagerInterface.php @@ -1,16 +1,37 @@ storage->load($this->id); + $this->assertTrue($restriction, 'User restriction exists in the database'); + $this->assertEqual($restriction->label(), $this->name, 'User restriction name matches'); + $this->assertEqual($restriction->getType(), $this->type, 'User restriction type matches'); + } + +} diff --git a/tests/src/Functional/UserRestrictionsExpireTest.php b/tests/src/Functional/UserRestrictionsExpireTest.php new file mode 100644 index 0000000..5c9d0a8 --- /dev/null +++ b/tests/src/Functional/UserRestrictionsExpireTest.php @@ -0,0 +1,42 @@ +storage->load($this->id); + $this->assertNotNull($restriction, 'User Restriction exists in the database'); + $this->assertEqual($restriction->label(), $this->name, 'User restriction exists'); + } + + /** + * Ensure an expired user may now log in. + */ + protected function testUserRestrictionsExpiredLogin() { + $account = $this->drupalCreateUser([], $this->name); + $this->drupalLogin($account); + } + + /** + * Ensure an expired restriction gets deleted on cron + */ + protected function testUserRestrictionsExpiredCron() { + \Drupal::service('cron')->run(); + $this->storage->resetCache($this->id); + $this->assertNull($this->storage->load($this->id), 'User restriction does not exist.'); + } + +} diff --git a/src/Tests/UserRestrictionsLoginTest.php b/tests/src/Functional/UserRestrictionsLoginTest.php similarity index 71% rename from src/Tests/UserRestrictionsLoginTest.php rename to tests/src/Functional/UserRestrictionsLoginTest.php index 1a44392..2711c3d 100644 --- a/src/Tests/UserRestrictionsLoginTest.php +++ b/tests/src/Functional/UserRestrictionsLoginTest.php @@ -1,33 +1,14 @@ 'User Restrictions Login test', - 'description' => 'Test the user restrictions rules for approved and denied names and emails.', - 'group' => 'User Restrictions', - ); - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Create some default User Restrictions - $this->createRestrictions(); - } - /** * Ensure a user cannot log in if their name is on the blacklist */ @@ -35,11 +16,11 @@ class UserRestrictionsLoginTest extends UserRestrictionsTestBase { $this->drupalGet('user/register'); $name = 'lol' . $this->randomName(); - $edit = array(); + $edit = []; $edit['name'] = $name; $edit['mail'] = $this->randomName() . '@example.com'; $this->drupalPostForm('user/register', $edit, t('Create new account')); - $this->assertText(t('The name @name is not allowed', array('@name' => $name)), 'User "name" restricted.'); + $this->assertText(t('The name %name is reserved, and cannot be used.', ['%name' => $name]), 'User "name" restricted.'); } /** @@ -50,7 +31,7 @@ class UserRestrictionsLoginTest extends UserRestrictionsTestBase { $this->drupalGet('user/register'); $name = 'lolcats'; - $edit = array(); + $edit = []; $edit['name'] = $name; $edit['mail'] = $this->randomName() . '@example.com'; $this->drupalPostForm('user/register', $edit, t('Create new account')); @@ -64,11 +45,11 @@ class UserRestrictionsLoginTest extends UserRestrictionsTestBase { $this->drupalGet('user/register'); $email = $this->randomName() . '@' . $this->randomName() . '.ru'; - $edit = array(); + $edit = []; $edit['name'] = $this->randomName(); $edit['mail'] = $email; $this->drupalPostForm('user/register', $edit, t('Create new account')); - $this->assertText(t('The mail @email is not allowed', array('@email' => $email)), 'User "email" restricted.'); + $this->assertText(t('The email %mail is reserved, and cannot be used.', ['%email' => $email]), 'User "email" restricted.'); } /** @@ -79,7 +60,7 @@ class UserRestrictionsLoginTest extends UserRestrictionsTestBase { $this->drupalGet('user/register'); $email = 'typhonius@mail.ru'; - $edit = array(); + $edit = []; $edit['name'] = $this->randomName(); $edit['mail'] = $email; $this->drupalPostForm('user/register', $edit, t('Create new account')); diff --git a/tests/src/Functional/UserRestrictionsTestBase.php b/tests/src/Functional/UserRestrictionsTestBase.php new file mode 100644 index 0000000..4042d06 --- /dev/null +++ b/tests/src/Functional/UserRestrictionsTestBase.php @@ -0,0 +1,44 @@ +storage = \Drupal::service('entity_type.manager') + ->getStorage('user_restrictions'); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Allow registration by site visitors without administrator approval. + $config = \Drupal::config('user.settings'); + $config->set('register', \USER_REGISTER_VISITORS)->save(); + } + +} diff --git a/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_1.yml b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_1.yml new file mode 100644 index 0000000..ac530d2 --- /dev/null +++ b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_1.yml @@ -0,0 +1,8 @@ +status: true +dependencies: { } +name: test_rule_1 +label: 'Test rule #1' +pattern: 'lol%' +access_type: '0' +rule_type: name +expiry: 0 diff --git a/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_2.yml b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_2.yml new file mode 100644 index 0000000..5f823c9 --- /dev/null +++ b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_2.yml @@ -0,0 +1,8 @@ +status: true +dependencies: { } +name: test_rule_2 +label: 'Test rule #2' +pattern: 'lolcats' +access_type: '1' +rule_type: name +expiry: 0 diff --git a/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_3.yml b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_3.yml new file mode 100644 index 0000000..ea5b637 --- /dev/null +++ b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_3.yml @@ -0,0 +1,8 @@ +status: true +dependencies: { } +name: test_rule_3 +label: 'Test rule #3' +pattern: '%@%.ru' +access_type: '0' +rule_type: mail +expiry: 0 diff --git a/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_4.yml b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_4.yml new file mode 100644 index 0000000..40f0d86 --- /dev/null +++ b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_4.yml @@ -0,0 +1,8 @@ +status: true +dependencies: { } +name: test_rule_4 +label: 'Test rule #4' +pattern: 'typhonius@mail.ru' +access_type: '1' +rule_type: mail +expiry: 0 diff --git a/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_expire_1.yml b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_expire_1.yml new file mode 100644 index 0000000..6473ecf --- /dev/null +++ b/tests/user_restrictions_test/config/install/user_restrictions.user_restrictions.test_rule_expire_1.yml @@ -0,0 +1,8 @@ +status: true +dependencies: { } +name: test_rule_expire_1 +label: 'Test rule with expiration #1' +pattern: 'expired-user' +access_type: '0' +rule_type: name +expiry: 1000 diff --git a/tests/user_restrictions_test/user_restrictions_test.info.yml b/tests/user_restrictions_test/user_restrictions_test.info.yml new file mode 100644 index 0000000..c360051 --- /dev/null +++ b/tests/user_restrictions_test/user_restrictions_test.info.yml @@ -0,0 +1,5 @@ +name: 'User Restrictions Test' +description: 'Specifies tests for module User restrictions.' +type: module +core: '8.x' +package: Testing diff --git a/user_restrictions.module b/user_restrictions.module index cdf993b..35d8408 100755 --- a/user_restrictions.module +++ b/user_restrictions.module @@ -4,7 +4,6 @@ * @file * Specifies rules for restricting the data users can set for their accounts. */ - use \Drupal\user_restrictions\UserRestrictionsManager; use \Drupal\Core\Form\FormStateInterface; @@ -14,8 +13,10 @@ use \Drupal\Core\Form\FormStateInterface; * Delete expired items in the user_restrictions table. */ function user_restrictions_cron() { - $entity_storage = \Drupal::getContainer()->get('entity_type.manager')->getStorage('user_restrictions'); - $user_restrictions = new UserRestrictionsManager($entity_storage); + $entity_manager = \Drupal::getContainer()->get('entity_type.manager'); + $user_restrictions_type_manager = \Drupal::getContainer()->get('user_restrictions.type_manager'); + $logger = \Drupal::getContainer()->get('logger.factory')->get('user_restrictions'); + $user_restrictions = new UserRestrictionsManager($entity_manager, $user_restrictions_type_manager, $logger); $user_restrictions->deleteExpiredRules(); } @@ -41,8 +42,8 @@ function user_restrictions_help($path, $arg) { $output .= '
' . t("Edit/Delete the restriction rules in /admin/config/people/user-restrictions.") . '
'; $output .= '
' . t("You can also test usernames and emails in the CHECK RULES fieldset that appears after at least one rule has been created.") . '
'; - return $output; - break; + return $output; + break; case 'admin/config/people/user-restrictions': return t("Set up rules for allowable usernames and e-mail address. A rule may either explicitly allow access or deny access based on the rule's Access type, Rule type, and Pattern. If the username or e-mail address of an existing account or new registration matches a deny rule, but not an allow rule, then the account will not be created (for new registrations) or able to log in (for existing accounts)."); break; @@ -50,24 +51,6 @@ function user_restrictions_help($path, $arg) { } /** - * Implements hook_permission(). - */ -function user_restrictions_permission() { - $perms = array( - 'administer user restrictions' => array( - 'title' => t('Administer User Restrictions'), - 'restrict access' => TRUE, - ), - 'bypass user restriction rules' => array( - 'title' => t('Bypass user restriction rules'), - 'restrict access' => TRUE, - ), - ); - - return $perms; -} - -/** * Implements hook_form_FORM_ID_alter() for user_login_form. */ function user_restrictions_form_user_login_form_alter(&$form, &$form_state) { @@ -88,26 +71,30 @@ function user_restrictions_form_user_form_alter(&$form, &$form_state) { $form['#validate'][] = 'user_restrictions_validate'; } - /** - * Validation function to pass off names/emails and determine if they are - * blocked or allowed on the site. + * Validation function to determine if the user is allowed on the site. * - * @param $form - * @param $form_state + * @param array $form + * Nested array of form elements that comprise the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. */ function user_restrictions_validate($form, FormStateInterface $form_state) { - if (!\Drupal::currentUser()->hasPermission('bypass user restriction rules')) { - $entity_storage = \Drupal::getContainer()->get('entity_type.manager')->getStorage('user_restrictions'); - $user_restrictions = new UserRestrictionsManager($entity_storage); + if (\Drupal::currentUser()->hasPermission('bypass user restriction rules')) { + return; + } - $user_restrictions->setName($form_state->getValue('name')); - $user_restrictions->setMail($form_state->getValue('mail')); + /** @var Drupal\user_restrictions\UserRestrictionsManagerInterface $restriction_manager */ + $restriction_manager = \Drupal::service('user_restrictions.manager'); - if ($user_restrictions->isRestricted()) { - foreach ($user_restrictions->getErrors() as $type => $message) { + if ($restriction_manager->matchesRestrictions($form_state->getValues())) { + foreach ($restriction_manager->getErrors() as $type => $message) { + if (isset($form[$type])) { $form_state->setErrorByName($form[$type], $message); } + else { + $form_state->setError($form, $message); + } } } } diff --git a/user_restrictions.permissions.yml b/user_restrictions.permissions.yml new file mode 100644 index 0000000..b7d7a51 --- /dev/null +++ b/user_restrictions.permissions.yml @@ -0,0 +1,6 @@ +administer user restrictions: + title: 'Administer user restrictions' + restrict access: true +bypass user restrictions rules: + title: 'Bypass user restrictions rules' + restrict access: true diff --git a/user_restrictions.services.yml b/user_restrictions.services.yml index 377cc76..b817857 100644 --- a/user_restrictions.services.yml +++ b/user_restrictions.services.yml @@ -1,4 +1,10 @@ services: + logger.channel.user_restrictions: + parent: logger.channel_base + arguments: ['user_restrictions'] + user_restrictions.type_manager: + class: Drupal\user_restrictions\UserRestrictionTypeManager + parent: default_plugin_manager user_restrictions.manager: class: Drupal\user_restrictions\UserRestrictionsManager - arguments: ['@entity.manager', '@logger.factory'] + arguments: ['@entity_type.manager', '@user_restrictions.type_manager', '@logger.channel.user_restrictions']