diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module index 2b1d8bf..79a24b4 100644 --- a/core/modules/contact/contact.module +++ b/core/modules/contact/contact.module @@ -86,82 +86,27 @@ 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; } /** - * Access callback: Checks access for a user's personal contact form. - * - * @param $account - * The user object of the user whose contact form is being requested. - * - * @see contact_menu() - */ -function _contact_personal_tab_access(UserInterface $account) { - global $user; - - // Anonymous users cannot have contact forms. - if ($account->isAnonymous()) { - return FALSE; - } - - // Users may not contact themselves. - if ($user->id() == $account->id()) { - return FALSE; - } - - // User administrators should always have access to personal contact forms. - if (user_access('administer users')) { - return TRUE; - } - - // If requested user has been blocked, do not allow users to contact them. - if ($account->isBlocked()) { - return FALSE; - } - - // If the requested user has disabled their contact form, do not allow users - // to contact them. - $account_data = Drupal::service('user.data')->get('contact', $account->id(), 'enabled'); - if (isset($account_data) && empty($account_data)) { - return FALSE; - } - // If the requested user did not save a preference yet, deny access if the - // configured default is disabled. - elseif (!Drupal::config('contact.settings')->get('user_default_enabled')) { - return FALSE; - } - - return user_access('access user contact forms'); -} - -/** * Implements hook_entity_bundle_info(). */ function contact_entity_bundle_info() { 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..0bfee5c --- /dev/null +++ b/core/modules/contact/lib/Drupal/contact/Access/ContactPageAccess.php @@ -0,0 +1,98 @@ +configFactory = $config_factory; + $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'); + // @todo revisit after https://drupal.org/node/2048223 + $user = \Drupal::currentUser(); + + // Anonymous users cannot have contact forms. + if ($contact_account->isAnonymous()) { + return static::DENY; + } + + // Users may not contact themselves. + if ($user->id() == $contact_account->id()) { + return static::DENY; + } + + // User administrators should always have access to personal contact forms. + if ($user->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. + else if (!$this->configFactory->get('contact.settings')->get('user_default_enabled')) { + return static::DENY; + } + + return $user->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..45b9b94 100644 --- a/core/modules/contact/lib/Drupal/contact/CategoryAccessController.php +++ b/core/modules/contact/lib/Drupal/contact/CategoryAccessController.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityAccessController; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; + /** * Defines an access controller for the contact category entity. * @@ -21,12 +22,18 @@ class CategoryAccessController extends EntityAccessController { * {@inheritdoc} */ public function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) { + $this->prepareUser($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..c59c484 --- /dev/null +++ b/core/modules/contact/lib/Drupal/contact/Controller/ContactPageController.php @@ -0,0 +1,141 @@ +flood = $flood; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('flood') + ); + } + + /** + * Presents the site-wide contact form. + * + * @param \Drupal\contact\CategoryInterface $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(CategoryInterface $contact_category = NULL) { + // Check if flood control has been activated for sending e-mails. + if (!$this->currentUser()->hasPermission('administer contact forms')) { + $this->contactFloodControl(); + } + + // Use the default category if no category has been passed. + if (empty($contact_category)) { + $contact_category = $this->entityManager() + ->getStorageController('contact_category') + ->load($this->config('contact.settings')->get('default_category')); + // If there are no categories, do not display the form. + if (empty($contact_category)) { + if ($this->currentUser()->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()->generateFromRoute('contact_category_add'))), 'error'); + return array(); + } + else { + throw new NotFoundHttpException(); + } + } + } + + $message = $this->entityManager() + ->getStorageController('contact_message') + ->create(array( + 'category' => $contact_category->id(), + )); + + $form = $this->entityManager()->getForm($message); + $form['#title'] = String::checkPlain($contact_category->label()); + return $form; + } + + /** + * Form constructor for the personal contact form. + * + * @param \Drupal\user\UserInterface $user + * The account for which a personal contact form should be generated. + * + * @return array + * The personal contact form as render array as expected by drupal_render(). + */ + public function contactPersonalPage(UserInterface $user) { + // Check if flood control has been activated for sending e-mails. + if (!$this->currentUser()->hasPermission('administer contact forms') && !$this->currentUser()->hasPermission('administer users')) { + $this->contactFloodControl(); + } + + $message = $this->entityManager()->getStorageController('contact_message')->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->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' => format_interval($interval), + )), 'error'); + throw new AccessDeniedHttpException(); + } + } + +} diff --git a/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php b/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php index e1943d5..851a450 100644 --- a/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php +++ b/core/modules/contact/lib/Drupal/contact/Plugin/views/field/ContactLink.php @@ -7,10 +7,14 @@ namespace Drupal\contact\Plugin\views\field; -use Drupal\Core\Entity\EntityInterface; use Drupal\Component\Annotation\PluginID; +use Drupal\Core\Access\StaticAccessCheckInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Routing\RouteProviderInterface; use Drupal\user\Plugin\views\field\Link; use Drupal\views\ResultRow; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Defines a field that links to the user contact page, if access is permitted. @@ -22,6 +26,53 @@ class ContactLink extends Link { /** + * The access manager for contact_personal_page route. + * + * @var \Drupal\Core\Access\StaticAccessCheckInterface + */ + protected $accessManager; + + /** + * The router provider interface. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * Constructs a Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Access\StaticAccessCheckInterface $access_manager + * The access manager for contact_personal_page route. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The router provider interface. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, StaticAccessCheckInterface $access_manager, RouteProviderInterface $route_provider) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->accessManager = $access_manager; + $this->routeProvider = $route_provider; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('access_check.contact_personal'), + $container->get('router.route_provider') + ); + } + + /** * {@inheritdoc} */ public function buildOptionsForm(&$form, &$form_state) { @@ -54,7 +105,10 @@ protected function renderLink(EntityInterface $entity, ResultRow $values) { $uid = $entity->id(); $path = "user/$uid/contact"; - if (!_contact_personal_tab_access($entity)) { + $request = Request::create($path); + $request->attributes->set('user', $entity); + $route = $this->routeProvider->getRouteByName('contact_personal_page'); + if ($this->accessManager->access($route, $request) !== StaticAccessCheckInterface::ALLOW) { return; } diff --git a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php index 8a9e210..05908c2 100644 --- a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php +++ b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php @@ -90,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. diff --git a/core/modules/contact/lib/Drupal/contact/Tests/MessageEntityTest.php b/core/modules/contact/lib/Drupal/contact/Tests/MessageEntityTest.php index baa6f3f..fd5300b 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', 'field'); + public static $modules = array('system', 'contact', 'field', 'user'); public static function getInfo() { return array(