ZF-Commons / zfc-rbac

Role-based access control module to provide additional features on top of Zend\Permissions\Rbac

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

EventManager::trigger() causes an event instance of the wrong type to be passed to registered listeners

nasko opened this issue · comments

Excuse me for submitting this here, but I was not able to comment directly in the #335 PR.

I've encountered a problem and because in my view this coin has two sides too, bringing it up here is just one of my options.

I'm using:

When the MvcEvent::EVENT_DISPATCH_ERROR is triggered in the AbstractGuard:onResult() method, all registered listeners receive a Zend\Mvc\MvcEvent event as their first argument.

However, when the event is handled in Zend\EventManager\EventManager::trigger(), the event is transformed into a Zend\EventManager\Event instance:

public function trigger($eventName, $target = null, $argv = [])
{
    $event = clone $this->eventPrototype; // Zend\EventManager\Event
    $event->setName($eventName);
    $event->setTarget($target);
    $event->setParams($argv);

    return $this->triggerListeners($event);
}

However, all registered listeners expect a Zend\Mvc\MvcEvent instance, but receive Zend\EventManager\Event, which causes fatal errors:

Catchable fatal error: Argument 1 passed to Zend\Mvc\View\Http\RouteNotFoundStrategy::detectNotFoundError() must be an instance of Zend\Mvc\MvcEvent, instance of Zend\EventManager\Event given, called in /var/www/projects/***/vendor/zendframework/zend-eventmanager/src/EventManager.php on line 271 and defined in /var/www/projects/***/vendor/zendframework/zend-mvc/src/View/Http/RouteNotFoundStrategy.php on line 135

Now, RouteNotFoundStrategy::detectNotFoundError() is just the first of a queue of 5 listeners, ZfcRbac\View\Strategy\RedirectStrategy being the last but one in the list.

Noticing this, I decided to simulate a request with faulty route - I requested http://my-applciation.dev/blabla, just to trace how the dispatch.error would be handled and how the above mentioned RouteNotFoundStrategy::detectNotFoundError() listener would handle the wrong type of the event instance. But, surprisingly the event passed to the listener was of the correct MvcEvent type.
Digging into the trace, I noticed that the event was triggered by Zend\EventManager\EventManager::triggerEvent() and not by ::trigger().

Here's the relevant section of Zend\Mvc\RouteListener:

public function onRoute(MvcEvent $event)
{
    // ...
    $target  = $event->getTarget();
    $results = $target->getEventManager()->triggerEvent($event);
    if (!empty($results)) {
        return $results->last();
    }

    return $event->getParams();
}

At first glance, the difference between EventManager::trigger() and EventManager::triggerEvent() is that the former transforms the type of the event instance and the latter merely triggers the registered listeners.

Looking at the history of the RouteListener class, I noticed that its implementation was changed recently with this commit:

@@ -44,9 +45,10 @@ public function onRoute($e)
          $routeMatch = $router->match($request);

          if (!$routeMatch instanceof Router\RouteMatch) {
 +            $e->setName(MvcEvent::EVENT_DISPATCH_ERROR);
              $e->setError(Application::ERROR_ROUTER_NO_MATCH);

 -            $results = $target->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $e);
 +            $results = $target->getEventManager()->triggerEvent($e);
              if (count($results)) {
                  return $results->last();
              }

Please forgive my long introduction, but my question is: shouldn't ZfcRbac use EventManager::triggerEvent() instead of trigger(), to address the apparently changed eventmanager implementation and additionally not to cause for a wrong event instance type to be passed over?

I browsed the ZfcRbac tests and obviously they pass, because a mock implementation of EventManager is being used and in addition, in the tests the listener is called directly:

$redirectStrategy->onError($mvcEvent);

The EventManager::triggerEvent(EventInterface $event) method has been introduced with this commit in late Sep 2015 together with two other discreet methods that should serve to replace a single call to EventManager::trigger(), which in turn prior to this implemented a resolution to one of multiple paths.

If we analyze the previous version of the code, namely this snippet:

        if ($event instanceof EventInterface) {
             $e        = $event;
             $event    = $e->getName();
             $callback = $target;
         } elseif ($target instanceof EventInterface) {
             $e = $target;
             $e->setName($event);
             $callback = empty($argv) ? null : $argv;
         } elseif ($argv instanceof EventInterface) {
             $e = $argv;
             $e->setName($event);
             $e->setTarget($target);
         } else {
             $e = clone $this->eventPrototype;
             $e->setName($event);
             $e->setTarget($target);
             $e->setParams($argv);
         }

... we see that the usage in ZfcRbac's AbstractGuard::onResult() falls into the second of the above tested scenarios:

         } elseif ($target instanceof EventInterface) {
             $e = $target;
             $e->setName($event);
             $callback = empty($argv) ? null : $argv;
         } 

Indeed:

    public function onResult(MvcEvent $event)
    {
        if ($this->isGranted($event)) {
            return;
        }
        $event->setError(self::GUARD_UNAUTHORIZED);
        $event->setParam('exception', new Exception\UnauthorizedException(
            'You are not authorized to access this resource',
            403
        ));
        $event->stopPropagation(true);
        $application  = $event->getApplication();
        $eventManager = $application->getEventManager();
        $eventManager->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $event);
    }

Here we pass the $event instance as the target param of the trigger() call, which makes it fall into the scenario where the triggerEvent() discreet method should be used, instead of trigger().

With trigger() the event type gets swapped (by means of $e = clone $this->eventPrototype;), whereas with triggerEvent() this is not the case.

I'm closing this, as ZfcRbac doesn't even support ZF3 yet... and that's when this issue kicks in.
Anyways, the fix is already merged into #335, and that was the context where the issue was valid to begin with.