Updated: Comment #N
Proposed commit message:
Issue #2185831 by tim.plunkett, dawehner: Split up ParamConverterManager and stop throwing NotFoundHttpException.

Problem/Motivation

The ParamConverterManager is used for both routing and access checks. However, when it fails to convert a parameter, it throws a NotFoundHttpException, which Drupal catches and returns a 404. This is not appropriate when used outside of the context of routing.

Additionally, the ParamConverterManager is supposed to just manage the parameter converter tagged services. However, it also uses these when acting as a RouteEnhancer.
This means that the AccessManager is asking the ParamConverterManager to enhance the routes, and it should be asking a dedicated RouteEnhancer.

Finally, this contributes to \Drupal::service('router')->match('/admin'); *always* throwing an exception. That's a known route with no params, provided by system module. We can currently *only* match by Request.

Proposed resolution

This splits up the class into two, by adding a ParamConversionEnhancer.

Remaining tasks

Figure out how to turn ParamNotConvertedException into NotFoundHttpException when appropriate.

User interface changes

N/A

API changes

Anyone calling ParamConverterManager::enhance should now call ParamConversionEnhancer::enhance. That is the route_enhancer.param_conversion service.

Comments

msonnabaum’s picture

Looks like a very nice improvement.

Status: Needs review » Needs work

The last submitted patch, param-converter-enhancer.patch, failed testing.

The last submitted patch, param-converter-enhancer.patch, failed testing.

tim.plunkett’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new16.83 KB
new45.78 KB

So all of these places are using NotFoundHttpException, but they're not always in an HTTP context.

However, at least some tests will fail, because if you go to /node/1/delete, delete it, and then to /node/1, you should get a 404, not ParamNotConvertedException.

So we need to figure out either a way to know which exception to throw, or a place to catch ParamNotConvertedException and throw NotFoundHttpException when in an HTTP context.

tim.plunkett’s picture

StatusFileSize
new1.51 KB
new47.15 KB

Per @msonnabaum's suggestion, I tried overriding the RouterListener, and it worked like a charm.

The last submitted patch, 4: paramconverter-2185831-4.patch, failed testing.

dawehner’s picture

I am a bit confused ... the main point of the param converter system is to upcast stuff, which is done in the enhance bit.
I would have argued that setRouteParameterConverters does not really belong on there, because this is the part also couples to the routing system.

In general can we agree that pure internal rewrites should not be "major"?

The last submitted patch, 4: paramconverter-2185831-4.patch, failed testing.

tim.plunkett’s picture

Issue summary: View changes
tim.plunkett’s picture

StatusFileSize
new30.49 KB
new49.68 KB

Okay, so we (@msonnabaum @Crell and I) talked through this a bunch more, mostly about which parts of this code are which object's responsibility.

The _raw_variables stuff is now part of the enhancer, and it better delegates to the manager.

This should help alleviate @dawehner's concern.

The interdiff is pretty useless IMO, reading the patch is probably easier.

tim.plunkett’s picture

Title: ParamConverterManager should not also be a RouteEnhancer » Split up ParamConverterManager and stop throwing NotFoundHttpException
tim.plunkett’s picture

StatusFileSize
new1.17 KB
new50 KB

I accidentally removed the test coverage for #2103145: ParameterConverter mangles raw values, adding that back :)

dawehner’s picture

I really like this patch now!

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php
    @@ -0,0 +1,34 @@
    +    }
    ...
    +  public function onKernelRequest(GetResponseEvent $event) {
    +    // Catch failed parameter conversions and throw a 404 instead. This cannot
    +    // be done on the router level as it does not always deal with HTTP.
    +    try {
    +      parent::onKernelRequest($event);
    +    }
    +    catch (ParamNotConvertedException $e) {
    +      throw new NotFoundHttpException('', $e);
    

    So does that mean that our router does not support something else than HTTP?

  2. +++ b/core/modules/edit/lib/Drupal/edit/Access/EditEntityAccessCheck.php
    @@ -65,14 +67,16 @@ protected function validateAndUpcastRequestAttributes(Request $request) {
       }
    
    +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php
    --- a/core/modules/edit/lib/Drupal/edit/Access/EditEntityAccessCheck.php
    +++ b/core/modules/edit/lib/Drupal/edit/Access/EditEntityAccessCheck.php
    
    +++ b/core/modules/edit/lib/Drupal/edit/Access/EditEntityAccessCheck.php
    +++ b/core/modules/edit/lib/Drupal/edit/Access/EditEntityAccessCheck.php
    @@ -44,7 +44,9 @@ public function access(Route $route, Request $request, AccountInterface $account
    
    @@ -44,7 +44,9 @@ public function access(Route $route, Request $request, AccountInterface $account
         // @todo Request argument validation and object loading should happen
         //   elsewhere in the request processing pipeline:
         //   http://drupal.org/node/1798214.
    -    $this->validateAndUpcastRequestAttributes($request);
    +    if (!$this->validateAndUpcastRequestAttributes($request)) {
    +      return static::KILL;
    +    }
     
         return $this->accessEditEntity($request->attributes->get('entity'), $account)  ? static::ALLOW : static::DENY;
       }
    @@ -65,14 +67,16 @@ protected function validateAndUpcastRequestAttributes(Request $request) {
    
    @@ -65,14 +67,16 @@ protected function validateAndUpcastRequestAttributes(Request $request) {
           $entity_id = $entity;
           $entity_type = $request->attributes->get('entity_type');
           if (!$entity_type || !$this->entityManager->getDefinition($entity_type)) {
    -        throw new NotFoundHttpException();
    +        return FALSE;
           }
           $entity = $this->entityManager->getStorageController($entity_type)->load($entity_id);
           if (!$entity) {
    -        throw new NotFoundHttpException();
    +        return FALSE;
           }
           $request->attributes->set('entity', $entity);
         }
    

    This is certainly a progress!

tim.plunkett’s picture

The new RouterListener is just enforcing the current assumptions made in HEAD, but moving them out of the router, and into the event subscriber that subscribes to KernelEvents::REQUEST.

It's not actually the RouterListener, but the RouterRequestSubscriber...
But that's now our naming, it's from Symfony.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Access/AccessManager.php
    @@ -91,10 +91,10 @@ class AccessManager extends ContainerAware {
    @@ -202,14 +202,14 @@ public function checkNamedRoute($route_name, array $parameters = array(), Accoun
    
    +++ b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php
    @@ -0,0 +1,34 @@
    +class RouterListener extends SymfonyRouterListener {
    ...
    +
    +/**
    + * Wraps the upstream request subscriber.
    

    It feels wrong that out RouterListener is aware of the ParamConverter, which is a subsystem that extending the router horizontally. Usually the param converter manager throws its exception because it is/was a route enhancer. Can't we achieve that by not hardcoding that into our RouterListener?

  2. +++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
    @@ -0,0 +1,62 @@
    +  }
    ...
    +
    +  /**
    +   * @var \Drupal\Core\ParamConverter\ParamConverterManagerInterface
    +   */
    +  protected $paramConverterManager;
    +
    +  /**
    +   * @param \Drupal\Core\ParamConverter\ParamConverterManagerInterface $param_converter_manager
    +   */
    +  public function __construct(ParamConverterManagerInterface $param_converter_manager) {
    +    $this->paramConverterManager = $param_converter_manager;
    ...
    +   * @param array $defaults
    +   *
    +   * @return \Symfony\Component\HttpFoundation\ParameterBag
    +   */
    

    A little bit more docs would be nice.

tim.plunkett’s picture

Issue summary: View changes
StatusFileSize
new49.88 KB
new7.05 KB

Thankfully @dawehner found a solution: GetResponseForExceptionEvent. It allows us to intercept and swap the exception, but only when from a request. Perfect!

I also improved the exception message for ParamNotConvertedException, and added more docs to ParamConversionEnhancer per #15.2.

Added a proposed commit message, since dawehner has worked through this with me the whole way.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
  1. +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php
    @@ -138,7 +138,7 @@ public function convert(array $defaults, Request $request) {
    -        throw new ParamNotConvertedException(sprintf('The "%s" parameter was not converted for the path "%s"', $name, $route->getPath()));
    +        throw new ParamNotConvertedException(sprintf('The "%s" parameter was not converted for the path "%s" (route name: "%s")', $name, $route->getPath(), $defaults[RouteObjectInterface::ROUTE_NAME]));
    

    <3

  2. +++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
    @@ -59,4 +70,24 @@ protected function copyRawVariables(array $defaults) {
    +    $events[KernelEvents::EXCEPTION][] = array('onException', 0);
    

    You don't really need to specify priority 0, but this is not commit-blocking at all.

Crell’s picture

The listener is way better than extending the class. Great work!

tim.plunkett’s picture

I almost started writing a change record called "NotFoundHttpException is never to be used in access checks", and then I realized I was just saying "no really, don't do that thing you were never ever supposed to do".

So I'm skipping that one.

Wrote up https://drupal.org/node/2188259 instead for the new exception.

yesct’s picture

I read the draft change record, edited it a tiny bit. Made sense to me.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed to 8.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

ParisLiakos’s picture

shouldnt the change record be published now? or there are other issues required for that?