diff --git a/core/includes/menu.inc b/core/includes/menu.inc index d205ac0..a31e892 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -970,6 +970,7 @@ function _menu_link_translate(&$item, $translate = FALSE) { function menu_item_route_access(Route $route, $href, &$map) { $request = Request::create('/' . $href); $request->attributes->set('_system_path', $href); + $request->attributes->set('_account', Drupal::request()->attributes->get('_account')); // Attempt to match this path to provide a fully built request to the // access checker. try { diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index 8515af0..442cc3e 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -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/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..ee9835d --- /dev/null +++ b/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php @@ -0,0 +1,99 @@ +contactSettings = $config_factory->get('contact.settings'); + $this->userData = $user_data; + } + + /** + * {@inheritdoc} + */ + public function appliesTo() { + // @see contact.routing.yml + 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. + $account_data = $this->userData->get('contact', $contact_account->id(), 'enabled'); + if (isset($account_data) && empty($account_data)) { + return static::DENY; + } + // If the requested user did not save a preference yet, deny access if the + // configured default is disabled. + elseif (!$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..d553119 --- /dev/null +++ b/core/modules/contact/lib/Drupal/contact/Controller/ContactPageController.php @@ -0,0 +1,198 @@ +flood = $flood; + $this->contactSettings = $config_factory->get('contact.settings'); + $this->entityManager = $entity_manager; + $this->contactStorage = $this->entityManager->getStorageController('contact_category'); + $this->urlGenerator = $url_generator; + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('flood'), + $container->get('config.factory'), + $container->get('plugin.manager.entity'), + $container->get('url_generator'), + $container->get('string_translation') + ); + } + + /** + * 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->translator->translate('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(); + } + + drupal_set_title(); + + $message = $this->contactStorage->create(array( + 'category' => 'personal', + 'recipient' => $user->id(), + )); + + $form = $this->entityManager->getForm($message); + $form['#title'] = $this->translator->translate('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->translator->translate('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/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/core/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index 4ee5df4..80e1673 100644 --- a/core/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/core/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -56,6 +56,7 @@ public function onKernelException(GetResponseForExceptionEvent $event) 'exception' => FlattenException::create($exception), 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, 'format' => $request->getRequestFormat(), + '_account' => $request->attributes->get('_account'), ); $request = $request->duplicate(null, null, $attributes);