0

I am debugging an event subscriber that checks if a user has access based on certain conditions.

If they don't have access, then I am doing:

throw new AccessDeniedHttpException();

However, this was initially causing this error:

Fatal error: Maximum function nesting level of '256' reached, aborting! in /vendor/symfony/http-foundation/ParameterBag.php on line 84

I noticed I did not have a 403 url defined in the Site Settings in the admin. Adding a node url here makes the error go away and everything works as expected.

Is there something about the way a 403 exception is handled that was causing an infinite loop here? Is this the right approach to handling a 403 in an event in Drupal?

edit:

Removing the 403 value in the admin settings, and changing it to a redirect response has the same effect. Drupal 8.6.3.

code:

<?php

namespace Drupal\permissions_by_term\Listener;

use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\permissions_by_term\Event\PermissionsByTermDeniedEvent;
use Drupal\permissions_by_term\Service\AccessCheck;
use Drupal\permissions_by_term\Service\AccessStorage;
use Drupal\permissions_by_term\Service\TermHandler;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Class KernelEventListener.
 *
 * @package Drupal\permissions_by_term
 */
class KernelEventListener implements EventSubscriberInterface
{

  /**
   * @var AccessCheck
   */
  private $accessCheckService;

  /**
   * @var TermHandler
   */
  private $term;

  /**
   * @var ContainerAwareEventDispatcher
   */
  private $eventDispatcher;

  /**
   * @var AccessStorage
   */
  private $accessStorageService;

  /**
   * Instantiating of objects on class construction.
   */
  public function __construct()
  {
    $this->accessCheckService = \Drupal::service('permissions_by_term.access_check');
    $this->accessStorageService = \Drupal::service('permissions_by_term.access_storage');
    $this->term = \Drupal::service('permissions_by_term.term_handler');
    $this->eventDispatcher = \Drupal::service('event_dispatcher');
  }

  /**
   * Access restriction on kernel request.
   */
  public function onKernelRequest(GetResponseEvent $event)
  {
    // Restricts access to nodes (views/edit).
    if ($this->canRequestGetNode($event->getRequest())) {
      $nid = $event->getRequest()->attributes->get('node')->get('nid')->getValue()['0']['value'];
      if (!$this->accessCheckService->canUserAccessByNodeId($nid, false, $this->accessStorageService->getLangCode($nid))) {
        $accessDeniedEvent = new PermissionsByTermDeniedEvent($nid);
        $this->eventDispatcher->dispatch(PermissionsByTermDeniedEvent::NAME, $accessDeniedEvent);

        $this->sendUserToAccessDeniedPage();
      }
    }

    // Restrict access to taxonomy terms by autocomplete list.
    if ($event->getRequest()->attributes->get('target_type') == 'taxonomy_term' &&
      $event->getRequest()->attributes->get('_route') == 'system.entity_autocomplete') {
      $query_string = $event->getRequest()->get('q');
      $query_string = trim($query_string);

      $tid = $this->term->getTermIdByName($query_string);

      $term = $this->term->getTerm();
      $termLangcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
      if ($term instanceof \Drupal\taxonomy\Entity\Term) {
        $termLangcode = $term->language()->getId();
      }

      if (!$this->accessCheckService->isAccessAllowedByDatabase($tid, \Drupal::currentUser()->id(), $termLangcode)) {
        $this->sendUserToAccessDeniedPage();
      }
    }
  }

  /**
   * Restricts access on kernel response.
   */
  public function onKernelResponse(FilterResponseEvent $event) {
    $this->restrictTermAccessAtAutoCompletion($event);
  }

  /**
   * Restricts access to terms on AJAX auto completion.
   */
  private function restrictTermAccessAtAutoCompletion(FilterResponseEvent $event) {
    if ($event->getRequest()->attributes->get('target_type') == 'taxonomy_term' &&
      $event->getRequest()->attributes->get('_route') == 'system.entity_autocomplete'
    ) {
      $json_suggested_terms = $event->getResponse()->getContent();
      $suggested_terms = json_decode($json_suggested_terms);
      $allowed_terms = [];
      foreach ($suggested_terms as $term) {
        $tid = $this->term->getTermIdByName($term->label);
        $termLangcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
        if ($this->term->getTerm() instanceof \Drupal\taxonomy\Entity\Term) {
          $termLangcode = $this->term->getTerm()->language()->getId();
        }

        if ($this->accessCheckService->isAccessAllowedByDatabase($tid, \Drupal::currentUser()->id(), $termLangcode)) {
          $allowed_terms[] = [
            'value' => $term->value,
            'label' => $term->label,
          ];
        }
      }

      $json_response = new JsonResponse($allowed_terms);
      $event->setResponse($json_response);
    }
  }

  /**
   * The subscribed events.
   */
  public static function getSubscribedEvents()
  {
    return [
      KernelEvents::REQUEST => 'onKernelRequest',
      KernelEvents::RESPONSE => 'onKernelResponse',
    ];
  }

  private function canRequestGetNode(Request $request) {
    if (method_exists($request->attributes, 'get') && !empty($request->attributes->get('node'))) {
      if (method_exists($request->attributes->get('node'), 'get')) {
        return TRUE;
      }
    }

    return FALSE;
  }

  private function sendUserToAccessDeniedPage() {
    throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
  }

}

I'm pitching in on this module.

  • 1
    Yes, you can build an infinite loop when the event subscriber throwing the exception is called again after the exception handler has returned a response. So check the response before throwing another 403. Also a themed 403 response is built in a sub request, so you should check that you are in the master request. It's hard to tell without seeing some code, but you should be able to find a better place to check access where such a loop is not possible. – 4k4 Nov 26 '18 at 22:10
  • Updated with code. – Kevin Nov 27 '18 at 15:34
  • 1
    Check this issue drupal.stackexchange.com/questions/232796/… – 4k4 Nov 27 '18 at 16:04
  • That worked - going to patch that in now. There is no side effect of checking master and exiting early, is there? – Kevin Nov 27 '18 at 16:53
  • No, it does exactly what you want, that the code is executed once. So you exit if it tries to run again. – 4k4 Nov 27 '18 at 17:13

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Browse other questions tagged or ask your own question.