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 agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.