diff --git a/core/core.services.yml b/core/core.services.yml
index 8a24ee7..ae05c2c 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -462,7 +462,7 @@ services:
calls:
- [setContainer, ['@service_container']]
exception_listener:
- class: Symfony\Component\HttpKernel\EventListener\ExceptionListener
+ class: Drupal\Core\EventSubscriber\ExceptionListener
tags:
- { name: event_subscriber }
arguments: [['@exception_controller', execute]]
diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php
index 8515af0..f3ca11d 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -12,7 +12,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\HttpKernel\Exception\FlattenException;
+use Symfony\Component\Debug\Exception\FlattenException;
use Drupal\Core\ContentNegotiation;
@@ -92,6 +92,7 @@ public function on403Html(FlattenException $exception, Request $request) {
}
$subrequest = Request::create('/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all());
+ $subrequest->attributes->set('_account', $request->attributes->get('_account'));
// The active trail is being statically cached from the parent request to
// the subrequest, like any other static. Unfortunately that means the
@@ -165,6 +166,7 @@ public function on404Html(FlattenException $exception, Request $request) {
// @todo The create() method expects a slash-prefixed path, but we store a
// normal system path in the site_404 variable.
$subrequest = Request::create('/' . $path, 'get', array(), $request->cookies->all(), array(), $request->server->all());
+ $subrequest->attributes->set('_account', $request->attributes->get('_account'));
// The active trail is being statically cached from the parent request to
// the subrequest, like any other static. Unfortunately that means the
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionListener.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionListener.php
new file mode 100644
index 0000000..94ec3b8
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionListener.php
@@ -0,0 +1,88 @@
+getException();
+ $request = $event->getRequest();
+
+ $this->logException($exception, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine()));
+
+ $request = $this->getRequestClone($exception, $request);
+
+ try {
+ $response = $event->getKernel()
+ ->handle($request, HttpKernelInterface::SUB_REQUEST, TRUE);
+ }
+ catch (\Exception $e) {
+ $this->logException($exception, sprintf('Exception thrown when handling an exception (%s: %s)', get_class($e), $e->getMessage()), FALSE);
+
+ // Set handling to false otherwise it wont be able to handle further more.
+ $handling = FALSE;
+
+ // Re-throw the exception from within HttpKernel as this is a catch-all.
+ return;
+ }
+
+ $event->setResponse($response);
+
+ $handling = FALSE;
+ }
+
+ /**
+ * Clones the request in case of an exception.
+ *
+ * @param \Exception $exception
+ * The thrown exception.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The original request.
+ *
+ * @return \Symfony\Component\HttpFoundation\Request
+ * The cloned request containing the exception and additional information.
+ */
+ protected function getRequestClone(\Exception $exception, Request $request) {
+ $attributes = array(
+ '_controller' => $this->controller,
+ 'exception' => FlattenException::create($exception),
+ 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : NULL,
+ 'format' => $request->getRequestFormat(),
+ // Include the current account.
+ '_account' => $request->attributes->get('_account')
+ );
+
+ $request = $request->duplicate(NULL, NULL, $attributes);
+ $request->setMethod('GET');
+ return $request;
+ }
+
+}
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 2b1d8bf..a1871c2 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -86,31 +86,21 @@ function contact_menu() {
$items['contact'] = array(
'title' => 'Contact',
- 'page callback' => 'contact_site_page',
- 'access arguments' => array('access site-wide contact form'),
+ 'route_name' => 'contact_site_page',
'menu_name' => 'footer',
- 'type' => MENU_SUGGESTED_ITEM,
- 'file' => 'contact.pages.inc',
);
$items['contact/%contact_category'] = array(
'title' => 'Contact category form',
'title callback' => 'entity_page_label',
'title arguments' => array(1),
- 'page callback' => 'contact_site_page',
- 'page arguments' => array(1),
- 'access arguments' => array('access site-wide contact form'),
+ 'route_name' => 'contact_site_page_category',
'type' => MENU_VISIBLE_IN_BREADCRUMB,
- 'file' => 'contact.pages.inc',
);
$items['user/%user/contact'] = array(
'title' => 'Contact',
- 'page callback' => 'contact_personal_page',
- 'page arguments' => array(1),
+ 'route_name' => 'contact_personal_page',
'type' => MENU_LOCAL_TASK,
- 'access callback' => '_contact_personal_tab_access',
- 'access arguments' => array(1),
'weight' => 2,
- 'file' => 'contact.pages.inc',
);
return $items;
}
diff --git a/core/modules/contact/contact.pages.inc b/core/modules/contact/contact.pages.inc
deleted file mode 100644
index b6da2ec..0000000
--- a/core/modules/contact/contact.pages.inc
+++ /dev/null
@@ -1,106 +0,0 @@
-get('default_category');
- if (isset($categories[$default_category])) {
- $category = $categories[$default_category];
- }
- // If there are no categories, do not display the form.
- else {
- if (user_access('administer contact forms')) {
- drupal_set_message(t('The contact form has not been configured. Add one or more categories to the form.', array('@add' => url('admin/structure/contact/add'))), 'error');
- return array();
- }
- else {
- throw new NotFoundHttpException();
- }
- }
- }
- else if ($category->id() == 'personal') {
- throw new NotFoundHttpException();
- }
- $message = entity_create('contact_message', array(
- 'category' => $category->id(),
- ));
- return Drupal::entityManager()->getForm($message);
-}
-
-/**
- * Page callback: Form constructor for the personal contact form.
- *
- * @param $recipient
- * The account for which a personal contact form should be generated.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
- *
- * @see contact_menu()
- * @see contact_personal_form_submit()
- *
- * @ingroup forms
- */
-function contact_personal_page($recipient) {
- global $user;
-
- // Check if flood control has been activated for sending e-mails.
- if (!user_access('administer contact forms') && !user_access('administer users')) {
- contact_flood_control();
- }
-
- drupal_set_title(t('Contact @username', array('@username' => user_format_name($recipient))), PASS_THROUGH);
-
- $message = entity_create('contact_message', array(
- 'recipient' => $recipient,
- 'category' => 'personal',
- ));
- return Drupal::entityManager()->getForm($message);
-}
-
-/**
- * Throws an exception if the current user is not allowed to submit a contact form.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
- *
- * @see contact_site_page()
- * @see contact_personal_page()
- */
-function contact_flood_control() {
- $config = Drupal::config('contact.settings');
- $limit = $config->get('flood.limit');
- $interval = $config->get('flood.interval');
- if (!Drupal::service('flood')->isAllowed('contact', $limit, $interval)) {
- drupal_set_message(t("You cannot send more than %limit messages in @interval. Try again later.", array(
- '%limit' => $limit,
- '@interval' => format_interval($interval),
- )), 'error');
- throw new AccessDeniedHttpException();
- }
-}
diff --git a/core/modules/contact/contact.routing.yml b/core/modules/contact/contact.routing.yml
index ba47a6a..ed18b20 100644
--- a/core/modules/contact/contact.routing.yml
+++ b/core/modules/contact/contact.routing.yml
@@ -1,9 +1,9 @@
contact_category_delete:
pattern: 'admin/structure/contact/manage/{contact_category}/delete'
defaults:
- _entity_form: contact_category.delete
+ _entity_form: 'contact_category.delete'
requirements:
- _entity_access: contact_category.delete
+ _entity_access: 'contact_category.delete'
contact_category_list:
pattern: '/admin/structure/contact'
@@ -15,13 +15,34 @@ contact_category_list:
contact_category_add:
pattern: '/admin/structure/contact/add'
defaults:
- _entity_form: contact_category.add
+ _entity_form: 'contact_category.add'
requirements:
_permission: 'administer contact forms'
contact_category_edit:
pattern: '/admin/structure/contact/manage/{contact_category}'
defaults:
- _entity_form: contact_category.edit
+ _entity_form: 'contact_category.edit'
requirements:
- _entity_access: contact_category.update
+ _entity_access: 'contact_category.update'
+
+contact_site_page:
+ pattern: 'contact'
+ defaults:
+ _content: '\Drupal\contact\Controller\ContactPageController::contactSitePage'
+ requirements:
+ _permission: 'access site-wide contact form'
+
+contact_site_page_category:
+ pattern: 'contact/{contact_category}'
+ defaults:
+ _content: '\Drupal\contact\Controller\ContactPageController::contactSitePage'
+ requirements:
+ _entity_access: 'contact_category.page'
+
+contact_personal_page:
+ pattern: 'user/{user}/contact'
+ defaults:
+ _content: '\Drupal\contact\Controller\ContactPageController::contactPersonalPage'
+ requirements:
+ _access_contact_personal_tab: 'TRUE'
diff --git a/core/modules/contact/contact.services.yml b/core/modules/contact/contact.services.yml
new file mode 100644
index 0000000..cccd1fd
--- /dev/null
+++ b/core/modules/contact/contact.services.yml
@@ -0,0 +1,6 @@
+services:
+ access_check.contact_personal:
+ class: Drupal\contact\Access\ContactPageAccess
+ tags:
+ - { name: access_check }
+ arguments: ['@config.factory', '@user.data']
diff --git a/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php b/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php
new file mode 100644
index 0000000..c278349
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php
@@ -0,0 +1,97 @@
+contactSettings = $config_factory->get('contact.settings');
+ $this->userData = $user_data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function appliesTo() {
+ return array('_access_contact_personal_tab');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access(Route $route, Request $request) {
+ $contact_account = $request->attributes->get('user');
+
+ // Anonymous users cannot have contact forms.
+ if ($contact_account->isAnonymous()) {
+ return static::DENY;
+ }
+
+ $current_account = $request->attributes->get('_account');
+
+ // Users may not contact themselves.
+ if ($current_account->id() == $contact_account->id()) {
+ return static::DENY;
+ }
+
+ // User administrators should always have access to personal contact forms.
+ if ($current_account->hasPermission('administer users')) {
+ return static::ALLOW;
+ }
+
+ // If requested user has been blocked, do not allow users to contact them.
+ if ($contact_account->isBlocked()) {
+ return static::DENY;
+ }
+
+ // If the requested user has disabled their contact form, do not allow users
+ // to contact them.
+ if ($this->userData->get('contact', $contact_account->id(), 'enabled') == 0) {
+ return static::DENY;
+ }
+ // If the requested user did not save a preference yet, deny access if the
+ // configured default is disabled.
+ else if (!$this->contactSettings->get('user_default_enabled')) {
+ return static::DENY;
+ }
+
+ return $current_account->hasPermission('access user contact forms') ? static::ALLOW : static::DENY;
+ }
+
+}
diff --git a/core/modules/contact/lib/Drupal/contact/CategoryAccessController.php b/core/modules/contact/lib/Drupal/contact/CategoryAccessController.php
index ba0bab7..294ca58 100644
--- a/core/modules/contact/lib/Drupal/contact/CategoryAccessController.php
+++ b/core/modules/contact/lib/Drupal/contact/CategoryAccessController.php
@@ -22,11 +22,16 @@ class CategoryAccessController extends EntityAccessController {
*/
public function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
if ($operation == 'delete' || $operation == 'update') {
- // Do not allow delete 'personal' category used for personal contact form.
- return user_access('administer contact forms', $account) && $entity->id() !== 'personal';
+ // Do not allow the 'personal' category to be deleted, as it's used for
+ // the personal contact form.
+ return $account->hasPermission('administer contact forms') && $entity->id() !== 'personal';
+ }
+ elseif ($operation == 'page') {
+ // Do not allow access personal category via site-wide route.
+ return $account->hasPermission('access site-wide contact form') && $entity->id() !== 'personal';
}
else {
- return user_access('administer contact forms', $account);
+ return $account->hasPermission('administer contact forms');
}
}
diff --git a/core/modules/contact/lib/Drupal/contact/Controller/ContactPageController.php b/core/modules/contact/lib/Drupal/contact/Controller/ContactPageController.php
new file mode 100644
index 0000000..3bc2e26
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/Controller/ContactPageController.php
@@ -0,0 +1,162 @@
+flood = $this->container->get('flood');
+ $this->contactSettings = $this->container->get('config.factory')->get('contact.settings');
+ $this->entityManager = $this->container->get('plugin.manager.entity');
+ $this->contactStorage = $this->entityManager->getStorageController('contact_category');
+ $this->urlGenerator = $this->container->get('url_generator');
+ }
+
+ /**
+ * Presents the site-wide contact form.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $_account
+ * The current account.
+ * @param \Drupal\contact\Plugin\Core\Entity\Category $contact_category
+ * The contact category to use.
+ *
+ * @return array
+ * The form as render array as expected by drupal_render().
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ * Exception is thrown when user tries to access non existing default
+ * contact category form.
+ */
+ public function contactSitePage(AccountInterface $_account, Category $contact_category = NULL) {
+ // Check if flood control has been activated for sending e-mails.
+ if (!$_account->hasPermission('administer contact forms')) {
+ $this->contactFloodControl();
+ }
+
+ // Use the default category if no category has been passed.
+ if (empty($contact_category)) {
+ $default_category = $this->contactSettings->get('default_category');
+ $contact_category = $this->contactStorage->load($default_category);
+ // If there are no categories, do not display the form.
+ if (empty($contact_category)) {
+ if ($_account->hasPermission('administer contact forms')) {
+ drupal_set_message($this->t('The contact form has not been configured. Add one or more categories to the form.', array(
+ '@add' => $this->urlGenerator->generateFromPath('admin/structure/contact/add'))), 'error');
+ return array();
+ }
+ else {
+ throw new NotFoundHttpException();
+ }
+ }
+ }
+
+ $message = $this->contactStorage->create(array(
+ 'category' => $contact_category->id(),
+ ));
+
+ return $this->entityManager->getForm($message);
+ }
+
+ /**
+ * Form constructor for the personal contact form.
+ *
+ * @param \Drupal\user\UserInterface $user
+ * The account for which a personal contact form should be generated.
+ * @param \Drupal\Core\Session\AccountInterface $_account
+ * The current user.
+ *
+ * @return array
+ * The personal contact form as render array as expected by drupal_render().
+ */
+ public function contactPersonalPage(UserInterface $user, AccountInterface $_account) {
+ // Check if flood control has been activated for sending e-mails.
+ if (!$_account->hasPermission('administer contact forms') && !$_account->hasPermission('administer users')) {
+ $this->contactFloodControl();
+ }
+
+ $message = $this->contactStorage->create(array(
+ 'category' => 'personal',
+ 'recipient' => $user->id(),
+ ));
+
+ $form = $this->entityManager->getForm($message);
+ $form['#title'] = $this->t('Contact @username', array('@username' => $user->getUsername()));
+ return $form;
+ }
+
+ /**
+ * Throws an exception if the current user triggers flood control.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+ */
+ protected function contactFloodControl() {
+ $limit = $this->contactSettings->get('flood.limit');
+ $interval = $this->contactSettings->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' => format_interval($interval),
+ )), 'error');
+ throw new AccessDeniedHttpException();
+ }
+ }
+
+}
diff --git a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
index 56b902e..5987167 100644
--- a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
+++ b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
@@ -44,7 +44,8 @@ function testSiteWideContact() {
$this->drupalLogin($admin_user);
$flood_limit = 3;
- \Drupal::config('contact.settings')
+ $this->container->get('config.factory')
+ ->get('contact.settings')
->set('flood.limit', $flood_limit)
->set('flood.interval', 600)
->save();
@@ -89,6 +90,9 @@ function testSiteWideContact() {
$this->drupalGet('contact');
$this->assertResponse(200);
$this->assertText(t('The contact form has not been configured.'));
+ // Test access personal category via site-wide contact page.
+ $this->drupalGet('contact/personal');
+ $this->assertResponse(403);
// Add categories.
// Test invalid recipients.
@@ -110,7 +114,8 @@ function testSiteWideContact() {
$this->assertRaw(t('Category %label has been added.', array('%label' => $label)));
// Check that the category was created in site default language.
- $langcode = \Drupal::config('contact.category.' . $id)->get('langcode');
+ $langcode = $this->container->get('config.factory')
+ ->get('contact.category.' . $id)->get('langcode');
$default_langcode = language_default()->id;
$this->assertEqual($langcode, $default_langcode);
@@ -119,7 +124,8 @@ function testSiteWideContact() {
// Test update contact form category.
$this->updateCategory($id, $label = $this->randomName(16), $recipients_str = implode(',', array($recipients[0], $recipients[1])), $reply = $this->randomName(30), FALSE);
- $config = \Drupal::config('contact.category.' . $id)->get();
+ $config = $this->container->get('config.factory')
+ ->get('contact.category.' . $id)->get();
$this->assertEqual($config['label'], $label);
$this->assertEqual($config['recipients'], array($recipients[0], $recipients[1]));
$this->assertEqual($config['reply'], $reply);
@@ -127,7 +133,10 @@ function testSiteWideContact() {
$this->assertRaw(t('Category %label has been updated.', array('%label' => $label)));
// Reset the category back to be the default category.
- \Drupal::config('contact.settings')->set('default_category', $id)->save();
+ $this->container->get('config.factory')
+ ->get('contact.settings')
+ ->set('default_category', $id)
+ ->save();
// Ensure that the contact form is shown without a category selection input.
user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form'));
@@ -182,7 +191,8 @@ function testSiteWideContact() {
$this->assertText(t('Message field is required.'));
// Test contact form with no default category selected.
- \Drupal::config('contact.settings')
+ $this->container->get('config.factory')
+ ->get('contact.settings')
->set('default_category', '')
->save();
$this->drupalGet('contact');
@@ -202,7 +212,10 @@ function testSiteWideContact() {
// Submit contact form one over limit.
$this->drupalGet('contact');
$this->assertResponse(403);
- $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => \Drupal::config('contact.settings')->get('flood.limit'), '@interval' => format_interval(600))));
+ $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array(
+ '%number' => $this->container->get('config.factory')->get('contact.settings')->get('flood.limit'),
+ '@interval' => format_interval(600))
+ ));
// Test listing controller.
$this->drupalLogin($admin_user);
@@ -376,7 +389,7 @@ function submitContact($name, $mail, $subject, $id, $message) {
$edit['mail'] = $mail;
$edit['subject'] = $subject;
$edit['message'] = $message;
- if ($id == \Drupal::config('contact.settings')->get('default_category')) {
+ if ($id == $this->container->get('config.factory')->get('contact.settings')->get('default_category')) {
$this->drupalPost('contact', $edit, t('Send message'));
}
else {
diff --git a/core/modules/contact/lib/Drupal/contact/Tests/MessageEntityTest.php b/core/modules/contact/lib/Drupal/contact/Tests/MessageEntityTest.php
index 49c73d7..10a6efd 100644
--- a/core/modules/contact/lib/Drupal/contact/Tests/MessageEntityTest.php
+++ b/core/modules/contact/lib/Drupal/contact/Tests/MessageEntityTest.php
@@ -21,7 +21,7 @@ class MessageEntityTest extends DrupalUnitTestBase {
*
* @var array
*/
- public static $modules = array('system', 'contact');
+ public static $modules = array('system', 'user', 'contact');
public static function getInfo() {
return array(
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionListenerTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionListenerTest.php
new file mode 100644
index 0000000..a8799bc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionListenerTest.php
@@ -0,0 +1,101 @@
+ 'Exception listener',
+ 'description' => 'Unit test the exception listener',
+ 'group' => 'System'
+ );
+ }
+
+ protected function setUp() {
+ $this->exceptionListener = new ExceptionListener('test_controller');
+ }
+
+ /**
+ * Tests the onException method with an account.
+ *
+ * @see \Drupal\Core\EventSubscriber\ExceptionListener::onException()
+ */
+ public function testOnException() {
+ // Setup a response with exception event and call onKernelException.
+
+ $account = $this->getMock('Drupal\Core\Session\AccountInterface');
+ $request = new Request(array(), array(), array('_account' => $account));
+ $kernel = new TestKernel();
+
+
+ try {
+ // Store the current error_log, and disable it temporarily to not fail
+ // on the created exception.
+ $error_log = ini_set('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul');
+
+ $exception = new \Exception('Test exception');
+ $event = new GetResponseForExceptionEvent($kernel, $request, 'test_request_type', $exception);
+ $this->exceptionListener->onKernelException($event);
+
+ // Restore the old error_log.
+ ini_set('error_log', $error_log);
+ }
+ catch (\Exception $e) {
+ $this->assertEquals('Test exception', $e->getMessage());
+ }
+
+ // Ensure that the subrequest has the proper account object.
+ $this->assertSame($account, $kernel->account, 'Account object could not be cloned.');
+ }
+
+}
+
+/**
+ * Defines a test kernel used for the test.
+ */
+class TestKernel implements HttpKernelInterface {
+
+ /**
+ * The current account.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ public $account;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) {
+ // Set the account to test in the actual test.
+ $this->account = $request->attributes->get('_account');
+ return new Response('foo');
+ }
+
+}