Skip to content
Snippets Groups Projects
Commit eebf4930 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2453341 by Wim Leers: Contact forms don't have necessary cache contexts...

Issue #2453341 by Wim Leers: Contact forms don't have necessary cache contexts & tags; flood control should work on validate, not on view
parent b58ab8ce
Branches
Tags
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
...@@ -62,7 +62,7 @@ public function access(UserInterface $user, AccountInterface $account) { ...@@ -62,7 +62,7 @@ public function access(UserInterface $user, AccountInterface $account) {
// Anonymous users cannot have contact forms. // Anonymous users cannot have contact forms.
if ($contact_account->isAnonymous()) { if ($contact_account->isAnonymous()) {
return AccessResult::forbidden(); return AccessResult::forbidden()->cachePerPermissions();
} }
// Users may not contact themselves. // Users may not contact themselves.
......
...@@ -7,16 +7,12 @@ ...@@ -7,16 +7,12 @@
namespace Drupal\contact\Controller; namespace Drupal\contact\Controller;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Flood\FloodInterface;
use Drupal\contact\ContactFormInterface; use Drupal\contact\ContactFormInterface;
use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RendererInterface;
use Drupal\user\UserInterface; use Drupal\user\UserInterface;
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
...@@ -24,20 +20,6 @@ ...@@ -24,20 +20,6 @@
*/ */
class ContactController extends ControllerBase { class ContactController extends ControllerBase {
/**
* The flood service.
*
* @var \Drupal\Core\Flood\FloodInterface
*/
protected $flood;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/** /**
* The renderer. * The renderer.
* *
...@@ -48,16 +30,10 @@ class ContactController extends ControllerBase { ...@@ -48,16 +30,10 @@ class ContactController extends ControllerBase {
/** /**
* Constructs a ContactController object. * Constructs a ContactController object.
* *
* @param \Drupal\Core\Flood\FloodInterface $flood
* The flood service.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date service.
* @param \Drupal\Core\Render\RendererInterface $renderer * @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer. * The renderer.
*/ */
public function __construct(FloodInterface $flood, DateFormatter $date_formatter, RendererInterface $renderer) { public function __construct(RendererInterface $renderer) {
$this->flood = $flood;
$this->dateFormatter = $date_formatter;
$this->renderer = $renderer; $this->renderer = $renderer;
} }
...@@ -66,8 +42,6 @@ public function __construct(FloodInterface $flood, DateFormatter $date_formatter ...@@ -66,8 +42,6 @@ public function __construct(FloodInterface $flood, DateFormatter $date_formatter
*/ */
public static function create(ContainerInterface $container) { public static function create(ContainerInterface $container) {
return new static( return new static(
$container->get('flood'),
$container->get('date.formatter'),
$container->get('renderer') $container->get('renderer')
); );
} }
...@@ -86,10 +60,6 @@ public static function create(ContainerInterface $container) { ...@@ -86,10 +60,6 @@ public static function create(ContainerInterface $container) {
* contact form. * contact form.
*/ */
public function contactSitePage(ContactFormInterface $contact_form = NULL) { public function contactSitePage(ContactFormInterface $contact_form = NULL) {
// Check if flood control has been activated for sending emails.
if (!$this->currentUser()->hasPermission('administer contact forms')) {
$this->contactFloodControl();
}
$config = $this->config('contact.settings'); $config = $this->config('contact.settings');
// Use the default form if no form has been passed. // Use the default form if no form has been passed.
...@@ -118,6 +88,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) { ...@@ -118,6 +88,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) {
$form = $this->entityFormBuilder()->getForm($message); $form = $this->entityFormBuilder()->getForm($message);
$form['#title'] = SafeMarkup::checkPlain($contact_form->label()); $form['#title'] = SafeMarkup::checkPlain($contact_form->label());
$form['#cache']['contexts'][] = 'user.permissions';
$this->renderer->addDependency($form, $config); $this->renderer->addDependency($form, $config);
return $form; return $form;
} }
...@@ -141,11 +112,6 @@ public function contactPersonalPage(UserInterface $user) { ...@@ -141,11 +112,6 @@ public function contactPersonalPage(UserInterface $user) {
throw new NotFoundHttpException(); throw new NotFoundHttpException();
} }
// Check if flood control has been activated for sending emails.
if (!$this->currentUser()->hasPermission('administer contact forms') && !$this->currentUser()->hasPermission('administer users')) {
$this->contactFloodControl();
}
$message = $this->entityManager()->getStorage('contact_message')->create(array( $message = $this->entityManager()->getStorage('contact_message')->create(array(
'contact_form' => 'personal', 'contact_form' => 'personal',
'recipient' => $user->id(), 'recipient' => $user->id(),
...@@ -153,24 +119,8 @@ public function contactPersonalPage(UserInterface $user) { ...@@ -153,24 +119,8 @@ public function contactPersonalPage(UserInterface $user) {
$form = $this->entityFormBuilder()->getForm($message); $form = $this->entityFormBuilder()->getForm($message);
$form['#title'] = $this->t('Contact @username', array('@username' => $user->getUsername())); $form['#title'] = $this->t('Contact @username', array('@username' => $user->getUsername()));
$form['#cache']['contexts'][] = 'user.permissions';
return $form; return $form;
} }
/**
* Throws an exception if the current user triggers flood control.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function contactFloodControl() {
$limit = $this->config('contact.settings')->get('flood.limit');
$interval = $this->config('contact.settings')->get('flood.interval');
if (!$this->flood->isAllowed('contact', $limit, $interval)) {
drupal_set_message($this->t('You cannot send more than %limit messages in @interval. Try again later.', array(
'%limit' => $limit,
'@interval' => $this->dateFormatter->formatInterval($interval),
)), 'error');
throw new AccessDeniedHttpException();
}
}
} }
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\contact; namespace Drupal\contact;
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Flood\FloodInterface; use Drupal\Core\Flood\FloodInterface;
...@@ -49,6 +50,13 @@ class MessageForm extends ContentEntityForm { ...@@ -49,6 +50,13 @@ class MessageForm extends ContentEntityForm {
*/ */
protected $mailHandler; protected $mailHandler;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/** /**
* Constructs a MessageForm object. * Constructs a MessageForm object.
* *
...@@ -60,12 +68,15 @@ class MessageForm extends ContentEntityForm { ...@@ -60,12 +68,15 @@ class MessageForm extends ContentEntityForm {
* The language manager service. * The language manager service.
* @param \Drupal\contact\MailHandlerInterface $mail_handler * @param \Drupal\contact\MailHandlerInterface $mail_handler
* The contact mail handler service. * The contact mail handler service.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date service.
*/ */
public function __construct(EntityManagerInterface $entity_manager, FloodInterface $flood, LanguageManagerInterface $language_manager, MailHandlerInterface $mail_handler) { public function __construct(EntityManagerInterface $entity_manager, FloodInterface $flood, LanguageManagerInterface $language_manager, MailHandlerInterface $mail_handler, DateFormatter $date_formatter) {
parent::__construct($entity_manager); parent::__construct($entity_manager);
$this->flood = $flood; $this->flood = $flood;
$this->languageManager = $language_manager; $this->languageManager = $language_manager;
$this->mailHandler = $mail_handler; $this->mailHandler = $mail_handler;
$this->dateFormatter = $date_formatter;
} }
/** /**
...@@ -76,7 +87,8 @@ public static function create(ContainerInterface $container) { ...@@ -76,7 +87,8 @@ public static function create(ContainerInterface $container) {
$container->get('entity.manager'), $container->get('entity.manager'),
$container->get('flood'), $container->get('flood'),
$container->get('language_manager'), $container->get('language_manager'),
$container->get('contact.mail_handler') $container->get('contact.mail_handler'),
$container->get('date.formatter')
); );
} }
...@@ -172,6 +184,28 @@ public function preview(array $form, FormStateInterface $form_state) { ...@@ -172,6 +184,28 @@ public function preview(array $form, FormStateInterface $form_state) {
$form_state->setRebuild(); $form_state->setRebuild();
} }
/**
* {@inheritdoc}
*/
public function validate(array $form, FormStateInterface $form_state) {
parent::validate($form, $form_state);
$message = $this->entity;
// Check if flood control has been activated for sending emails.
if (!$this->currentUser()->hasPermission('administer contact forms') && (!$message->isPersonal() || !$this->currentUser()->hasPermission('administer users'))) {
$limit = $this->config('contact.settings')->get('flood.limit');
$interval = $this->config('contact.settings')->get('flood.interval');
if (!$this->flood->isAllowed('contact', $limit, $interval)) {
$form_state->setErrorByName('', $this->t('You cannot send more than %limit messages in @interval. Try again later.', array(
'%limit' => $limit,
'@interval' => $this->dateFormatter->formatInterval($interval),
)));
}
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -152,11 +152,13 @@ function testPersonalContactAccess() { ...@@ -152,11 +152,13 @@ function testPersonalContactAccess() {
// Test that anonymous users can access admin user's contact form. // Test that anonymous users can access admin user's contact form.
$this->drupalGet('user/' . $this->adminUser->id() . '/contact'); $this->drupalGet('user/' . $this->adminUser->id() . '/contact');
$this->assertResponse(200); $this->assertResponse(200);
$this->assertCacheContext('user.permissions');
// Revoke the personal contact permission for the anonymous user. // Revoke the personal contact permission for the anonymous user.
user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access user contact forms')); user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access user contact forms'));
$this->drupalGet('user/' . $this->contactUser->id() . '/contact'); $this->drupalGet('user/' . $this->contactUser->id() . '/contact');
$this->assertResponse(403); $this->assertResponse(403);
$this->assertCacheContext('user.permissions');
$this->drupalGet('user/' . $this->adminUser->id() . '/contact'); $this->drupalGet('user/' . $this->adminUser->id() . '/contact');
$this->assertResponse(403); $this->assertResponse(403);
...@@ -235,7 +237,7 @@ function testPersonalContactFlood() { ...@@ -235,7 +237,7 @@ function testPersonalContactFlood() {
} }
// Submit contact form one over limit. // Submit contact form one over limit.
$this->drupalGet('user/' . $this->contactUser->id(). '/contact'); $this->submitPersonalContact($this->contactUser);
$this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $flood_limit, '@interval' => \Drupal::service('date.formatter')->formatInterval($this->config('contact.settings')->get('flood.interval')))), 'Normal user denied access to flooded contact form.'); $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $flood_limit, '@interval' => \Drupal::service('date.formatter')->formatInterval($this->config('contact.settings')->get('flood.interval')))), 'Normal user denied access to flooded contact form.');
// Test that the admin user can still access the contact form even though // Test that the admin user can still access the contact form even though
......
...@@ -230,8 +230,7 @@ function testSiteWideContact() { ...@@ -230,8 +230,7 @@ function testSiteWideContact() {
$this->assertText(t('Your message has been sent.')); $this->assertText(t('Your message has been sent.'));
} }
// Submit contact form one over limit. // Submit contact form one over limit.
$this->drupalGet('contact'); $this->submitContact($this->randomMachineName(16), $recipients[0], $this->randomMachineName(16), $id, $this->randomMachineName(64));
$this->assertResponse(403);
$this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'), '@interval' => \Drupal::service('date.formatter')->formatInterval(600)))); $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'), '@interval' => \Drupal::service('date.formatter')->formatInterval(600))));
// Test listing controller. // Test listing controller.
......
...@@ -2740,6 +2740,17 @@ protected function buildUrl($path, array $options = array()) { ...@@ -2740,6 +2740,17 @@ protected function buildUrl($path, array $options = array()) {
} }
} }
/**
* Asserts whether an expected cache context was present in the last response.
*
* @param string $expected_cache_context
* The expected cache context.
*/
protected function assertCacheContext($expected_cache_context) {
$cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
$this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
}
/** /**
* Asserts whether an expected cache tag was present in the last response. * Asserts whether an expected cache tag was present in the last response.
* *
...@@ -2748,7 +2759,7 @@ protected function buildUrl($path, array $options = array()) { ...@@ -2748,7 +2759,7 @@ protected function buildUrl($path, array $options = array()) {
*/ */
protected function assertCacheTag($expected_cache_tag) { protected function assertCacheTag($expected_cache_tag) {
$cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')); $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
$this->assertTrue(in_array($expected_cache_tag, $cache_tags)); $this->assertTrue(in_array($expected_cache_tag, $cache_tags), "'" . $expected_cache_tag . "' is present in the X-Drupal-Cache-Tags header.");
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment