Commit db41da45 authored by git's avatar git Committed by larowlan

Issue #2724507 by Bambell, larowlan, Berdir, andypost: Add a maximum submission limit for forms

parent aa5a35d7
......@@ -14,6 +14,9 @@ contact.form.*.third_party.contact_storage:
disabled_form_message:
type: string
label: 'Disabled contact form message'
maximum_submissions_user:
type: integer
label: 'Maximum submission limit per user'
field.storage_settings.contact_storage_options_email:
type: mapping
......
......@@ -7,6 +7,7 @@
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Implements hook_install().
......@@ -62,3 +63,23 @@ function contact_storage_update_8002() {
->create($storage->read('system.action.message_delete_action'))
->save();
}
/**
* Defines fields for the user id and ip address, for the contact messages.
*/
function contact_storage_update_8003() {
$storage_definition = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User ID'))
->setDescription(t('The user ID.'))
->setSetting('target_type', 'contact_form')
->setDefaultValueCallback('contact_storage_contact_message_default_uid');
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('uid', 'contact_message', 'contact_storage', $storage_definition);
$storage_definition = BaseFieldDefinition::create('string')
->setLabel(t('IP address'))
->setDescription(t('The IP address of the submitter.'))
->setDefaultValueCallback('contact_storage_contact_message_default_ip_address');
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('ip_address', 'contact_message', 'contact_storage', $storage_definition);
}
......@@ -53,6 +53,12 @@ function contact_storage_form_contact_form_form_alter(&$form, FormStateInterface
'#description' => t('Show the preview button?'),
'#default_value' => $contact_form->getThirdPartySetting('contact_storage', 'show_preview', TRUE),
];
$form['contact_storage_maximum_submissions_user'] = [
'#type' => 'textfield',
'#title' => t('Maximum submissions'),
'#description' => t('The maximum number of times, per user, the form can be submitted (0 for unlimited).'),
'#default_value' => $contact_form->getThirdPartySetting('contact_storage', 'maximum_submissions_user', 0),
];
$form['#entity_builders'][] = 'contact_storage_contact_form_form_builder';
}
......@@ -66,6 +72,7 @@ function contact_storage_contact_form_form_builder($entity_type, ContactFormInte
$contact_form->setThirdPartySetting('contact_storage', 'submit_text', $form_state->getValue('contact_storage_submit_text'));
$contact_form->setThirdPartySetting('contact_storage', 'show_preview', $form_state->getValue('contact_storage_preview'));
$contact_form->setThirdPartySetting('contact_storage', 'disabled_form_message', $form_state->getValue('contact_storage_disabled_form_message'));
$contact_form->setThirdPartySetting('contact_storage', 'maximum_submissions_user', $form_state->getValue('contact_storage_maximum_submissions_user'));
}
/**
......@@ -88,6 +95,22 @@ function contact_storage_form_contact_message_form_alter(&$form, &$form_state, $
$form['actions']['preview']['#access'] = FALSE;
}
}
// Check if the current user has reached the form's maximum submission limit.
$maximum_submissions_user = $contact_form->getThirdPartySetting('contact_storage', 'maximum_submissions_user', 0);
if (($maximum_submissions_user !== 0) && contact_storage_maximum_submissions_user($contact_form) >= $maximum_submissions_user) {
// Sets the error message.
$form['maximum_submissions_error'] = array(
'#type' => 'container',
'#markup' => t('You have reached the maximum submission limit of @limit for this form.', ['@limit' => $maximum_submissions_user]),
'#attributes' => array(
'class' => array('messages', 'messages--error'),
),
'#weight' => -100,
);
// Remove the submit and preview buttons.
$form['actions']['submit']['#access'] = FALSE;
$form['actions']['preview']['#access'] = FALSE;
}
}
/**
......@@ -125,10 +148,41 @@ function contact_storage_entity_base_field_info(EntityTypeInterface $entity_type
->setTranslatable(TRUE)
->setReadOnly(TRUE);
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User ID'))
->setDescription(t('The user ID.'))
->setSetting('target_type', 'contact_form')
->setDefaultValueCallback('contact_storage_contact_message_default_uid');
$fields['ip_address'] = BaseFieldDefinition::create('string')
->setLabel(t('IP address'))
->setDescription(t('The IP address of the submitter.'))
->setDefaultValueCallback('contact_storage_contact_message_default_ip_address');
return $fields;
}
}
/**
* Default value callback for the contact message uid field.
*
* @return int
* The user ID.
*/
function contact_storage_contact_message_default_uid() {
return \Drupal::currentUser()->id();
}
/**
* Default value callback for the contact message ip_address field.
*
* @return int
* The client IP address.
*/
function contact_storage_contact_message_default_ip_address() {
return \Drupal::request()->getClientIp();
}
/**
* Implements hook_entity_base_field_info_alter().
*/
......@@ -141,6 +195,9 @@ function contact_storage_entity_base_field_info_alter(&$fields, EntityTypeInterf
$fields[$field_name]->setDisplayOptions('view', array('weight' => $i));
$i++;
}
// Add a validation constraint to prevent form submission if the limit is
// reached.
$fields['message']->addConstraint('ConstactStorageMaximumSubmissions', []);
}
}
......@@ -324,3 +381,34 @@ function contact_storage_field_widget_info_alter(&$info) {
// Let our options_email field type re-use the default options widget.
$info['options_select']['field_types'][] = 'contact_storage_options_email';
}
/**
* Returns the number of times the current user has submitted the specified
* form.
*
* @param Drupal\contact\ContactFormInterface $contact_form
* The contact_form entity.
*
* @return int
* The number of times the current user has submitted the specified form.
*/
function contact_storage_maximum_submissions_user(ContactFormInterface $contact_form) {
$account = \Drupal::currentUser();
if ($account->isAnonymous()) {
// Anonymous user, limit per submission with the same IP address.
$ip_address = \Drupal::request()->getClientIp();
$query = \Drupal::entityQuery('contact_message')
->condition('contact_form', $contact_form->id())
->condition('ip_address', $ip_address)
->condition('uid', $account->id());
return count($query->execute());
}
else {
// Limit per submission with the same uid.
$query = \Drupal::entityQuery('contact_message')
->condition('contact_form', $contact_form->id())
->condition('uid', $account->id());
return count($query->execute());
}
}
<?php
namespace Drupal\contact_storage\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Verify that the form has not been submitted more times that the limit.
*
* @Constraint(
* id = "ConstactStorageMaximumSubmissions",
* label = @Translation("Maximum submission limit", context = "Validation"),
* )
*/
class ConstactStorageMaximumSubmissionsConstraint extends Constraint {
/**
* Message shown when the maximum submission limit has been reached.
*
* @var string
*/
public $limitReached = 'You have reached the maximum submission limit of @limit for this form.';
}
<?php
namespace Drupal\contact_storage\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the maximum submission limit constraint.
*/
class ConstactStorageMaximumSubmissionsConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
// Check if the current user has reached the form's maximum submission limit.
$contact_form = $entity->getParent()->get('contact_form')->referencedEntities()[0];
$maximum_submissions_user = $contact_form->getThirdPartySetting('contact_storage', 'maximum_submissions_user', 0);
if (($maximum_submissions_user !== 0) && contact_storage_maximum_submissions_user($contact_form) >= $maximum_submissions_user) {
// Limit reached; can't submit the form.
$this->context->addViolation($constraint->limitReached, ['@limit' => $maximum_submissions_user]);
}
}
}
......@@ -14,6 +14,13 @@ class ContactStorageTest extends ContactStorageTestBase {
use FieldUiTestTrait;
/**
* An administrative user with permission to administer contact forms.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
......@@ -29,15 +36,15 @@ class ContactStorageTest extends ContactStorageTestBase {
'contact_storage',
);
/**
* Tests contact messages submitted through contact form.
*/
public function testContactStorage() {
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_actions_block');
$this->drupalPlaceBlock('page_title_block');
// Create and login administrative user.
$admin_user = $this->drupalCreateUser(array(
$this->adminUser = $this->drupalCreateUser([
'access site-wide contact form',
'administer contact forms',
'administer users',
......@@ -45,8 +52,14 @@ class ContactStorageTest extends ContactStorageTestBase {
'administer contact_message fields',
'administer contact_message form display',
'administer contact_message display',
));
$this->drupalLogin($admin_user);
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests contact messages submitted through contact form.
*/
public function testContactStorage() {
// Create first valid contact form.
$mail = 'simpletest@example.com';
$this->addContactForm('test_id', 'test_label', $mail, '', TRUE);
......@@ -67,7 +80,7 @@ class ContactStorageTest extends ContactStorageTestBase {
$this->assertTrue(count($captured_emails) === 1);
// Login as admin.
$this->drupalLogin($admin_user);
$this->drupalLogin($this->adminUser);
// Verify that the global setting stating whether e-mails should be sent in
// HTML format is false by default.
......@@ -146,7 +159,7 @@ class ContactStorageTest extends ContactStorageTestBase {
$this->assertText('There is no Contact message yet.');
// Fill the redirect field and assert the page is successfully redirected.
$edit = ['contact_storage_uri' => 'entity:user/' . $admin_user->id()];
$edit = ['contact_storage_uri' => 'entity:user/' . $this->adminUser->id()];
$this->drupalPostForm('admin/structure/contact/manage/test_id', $edit, t('Save'));
$edit = [
'subject[0][value]' => 'Test subject',
......@@ -154,7 +167,7 @@ class ContactStorageTest extends ContactStorageTestBase {
];
$this->drupalPostForm('contact', $edit, t('Send message'));
$this->assertText('Your message has been sent.');
$this->assertEqual($this->url, $admin_user->urlInfo()->setAbsolute()->toString());
$this->assertEqual($this->url, $this->adminUser->urlInfo()->setAbsolute()->toString());
// Check that this new message is now in HTML format.
$captured_emails = $this->drupalGetMails();
......@@ -295,4 +308,28 @@ class ContactStorageTest extends ContactStorageTestBase {
$this->assertText('custom disabled message');
}
public function testMaximumSubmissionLimit() {
// Create a new contact form with a maximum submission limit of 2.
$this->addContactForm('test_id_3', 'test_label', 'simpletest@example.com', '', FALSE, ['contact_storage_maximum_submissions_user' => 2]);
$this->assertText(t('Contact form test_label has been added.'));
// Sends 2 messages with "Send yourself a copy" option activated, shouldn't
// reach the limit even if 2 messages are sent twice.
$this->drupalGet('contact/test_id_3');
$edit = [
'subject[0][value]' => 'Test subject',
'message[0][value]' => 'Test message',
'copy' => 'checked',
];
$this->drupalPostForm(NULL, $edit, t('Send message'));
$this->assertText(t('Your message has been sent.'));
$this->drupalGet('contact/test_id_3');
$this->drupalPostForm(NULL, $edit, t('Send message'));
$this->assertText(t('Your message has been sent.'));
// Try accessing the form after the limit has been reached.
$this->drupalGet('contact/test_id_3');
$this->assertText(t('You have reached the maximum submission limit of 2 for this form.'));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment