public function testDoesNotLimitDispatchErrorEventToOnlyOneListener() { $eventManager = new EventManager(); $application = $this->prophesize(Application::class); $application->getEventManager()->willReturn($eventManager); $event = new MvcEvent(); $event->setApplication($application->reveal()); $guard = new DummyGuard(); $guard->attach($eventManager); $eventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, function (MvcEvent $event) { $event->setParam('first-listener', true); }); $eventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, function (MvcEvent $event) { $event->setParam('second-listener', true); }); // attach listener with lower priority than DummyGuard $eventManager->attach(MvcEvent::EVENT_ROUTE, function (MvcEvent $event) { $this->fail('should not be called, because guard should stop propagation'); }, DummyGuard::EVENT_PRIORITY - 1); $event->setName(MvcEvent::EVENT_ROUTE); $eventManager->triggerEvent($event); $this->assertTrue($event->getParam('first-listener')); $this->assertTrue($event->getParam('second-listener')); $this->assertTrue($event->propagationIsStopped()); }
public function testOnBootstrapListenersWithHttpRequest() { $module = new Module(); $application = $this->createApplication(); $sm = $application->getServiceManager(); $sm->setService('FilterManager', new FilterPluginManager()); foreach ($module->getServiceConfig()['invokables'] as $key => $value) { $sm->setInvokableClass($key, $value); } foreach ($module->getServiceConfig()['factories'] as $key => $value) { $sm->setFactory($key, $value); } $sm->get('ViewHelperManager')->setService('bodyClass', $this->getMock(BodyClass::class)); $event = new MvcEvent(); $event->setApplication($application); $em = $application->getEventManager(); $em->getSharedManager()->clearListeners(LayoutUpdater::class); $module->onBootstrap($event); $layoutUpdater = $sm->get(LayoutUpdaterInterface::class); $this->assertEquals(['default'], $layoutUpdater->getHandles()); $mvcEvent = new MvcEvent(); $mvcEvent->setApplication($application); $mvcEvent->setName(MvcEvent::EVENT_DISPATCH_ERROR); $mvcEvent->setError('test-error'); $em->triggerEvent($mvcEvent); $this->assertEquals(['default', 'test-error'], $layoutUpdater->getHandles()); }
/** * Listen to the "route" event and attempt to route the request * * If no matches are returned, triggers "dispatch.error" in order to * create a 404 response. * * Seeds the event with the route match on completion. * * @param MvcEvent $event * @return null|RouteMatch */ public function onRoute(MvcEvent $event) { $request = $event->getRequest(); $router = $event->getRouter(); $routeMatch = $router->match($request); if ($routeMatch instanceof RouteMatch) { $event->setRouteMatch($routeMatch); return $routeMatch; } $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); $event->setError(Application::ERROR_ROUTER_NO_MATCH); $target = $event->getTarget(); $results = $target->getEventManager()->triggerEvent($event); if (!empty($results)) { return $results->last(); } return $event->getParams(); }
/** * @param MvcEvent $e */ public function onBootstrap(MvcEvent $e) { ini_set('display_errors', 'off'); $errorCallback = function () use($e) { // Fetching the error's information $error = error_get_last() ?: func_get_args(); $error = array_values($error); if (!$error) { return; } // Create exception ans associated event $exception = new ErrorException($error[1], 0, $error[0], $error[2], $error[3], isset($error[4]) && $error[4] instanceof \Exception ? $error[4] : null); $e->setParam('exception', $exception); $e->setError(Application::ERROR_EXCEPTION); $e->setName(MvcEvent::EVENT_RENDER_ERROR); /** @var Application $application */ $application = $e->getApplication(); $application->getEventManager()->triggerEvent($e); }; // Set error handlers set_error_handler($errorCallback, \E_ALL); register_shutdown_function($errorCallback); }
/** * Marshal a middleware not callable exception event * * @param string $type * @param string $middlewareName * @param MvcEvent $event * @param Application $application * @param \Exception $exception * @return mixed */ protected function marshalMiddlewareNotCallable($type, $middlewareName, MvcEvent $event, Application $application, \Exception $exception = null) { $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); $event->setError($type); $event->setController($middlewareName); $event->setControllerClass('Middleware not callable: ' . $middlewareName); if ($exception !== null) { $event->setParam('exception', $exception); } $events = $application->getEventManager(); $results = $events->triggerEvent($event); $return = $results->last(); if (!$return) { $return = $event->getResult(); } return $return; }
/** * Marshal a bad controller exception event * * @param string $controllerName * @param MvcEvent $event * @param Application $application * @param \Throwable|\Exception $exception * @return mixed */ protected function marshalBadControllerEvent($controllerName, MvcEvent $event, Application $application, $exception) { $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); $event->setError($application::ERROR_EXCEPTION); $event->setController($controllerName); $event->setParam('exception', $exception); $events = $application->getEventManager(); $results = $events->triggerEvent($event); $return = $results->last(); if (!$return) { return $event->getResult(); } return $return; }
/** * Render the view * * @param MvcEvent $e * @return Response|null * @throws \Exception */ public function render(MvcEvent $e) { $result = $e->getResult(); if ($result instanceof Response) { return $result; } // Martial arguments $request = $e->getRequest(); $response = $e->getResponse(); $viewModel = $e->getViewModel(); if (!$viewModel instanceof ViewModel) { return; } $view = $this->view; $view->setRequest($request); $view->setResponse($response); $caughtException = null; try { $view->render($viewModel); } catch (\Throwable $ex) { $caughtException = $ex; } catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced $caughtException = $ex; } if ($caughtException !== null) { if ($e->getName() === MvcEvent::EVENT_RENDER_ERROR) { throw $caughtException; } $application = $e->getApplication(); $events = $application->getEventManager(); $e->setError(Application::ERROR_EXCEPTION); $e->setParam('exception', $caughtException); $e->setName(MvcEvent::EVENT_RENDER_ERROR); $events->triggerEvent($e); } return $response; }
/** * @todo handle with view strategy * @param Application $app * @param MvcEvent $event * @return void */ protected function triggerUnauthorizedError(Application $app, MvcEvent $event) { $app->getServiceManager()->get('response')->setStatusCode(403); $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); $event->setError('error-unauthorized-route'); $event->setParam('exception', new PermissionDeniedException('You are not authorized to access this page.', 403)); return $app->getEventManager()->triggerEvent($event); }
/** * Handle an exception/throwable. * * @param Throwable|Exception $exception * @param MvcEvent $event * @param EventManagerInterface $events * @return self */ private function handleException($exception, MvcEvent $event, EventManagerInterface $events) { $event->setName(MvcEvent::EVENT_DISPATCH_ERROR); $event->setError(self::ERROR_EXCEPTION); $event->setParam('exception', $exception); $result = $events->triggerEvent($event); $response = $result->last(); if ($response instanceof ResponseInterface) { $event->setName(MvcEvent::EVENT_FINISH); $event->setTarget($this); $event->setResponse($response); $this->response = $response; $events->triggerEvent($event); return $this; } return $this->completeRequest($event); }
/** * Attempt to validate the incoming request * * If an input filter is associated with the matched controller service, * attempt to validate the incoming request, and inject the event with the * input filter, as the "ZF\ContentValidation\InputFilter" parameter. * * Uses the ContentNegotiation ParameterDataContainer to retrieve parameters * to validate, and returns an ApiProblemResponse when validation fails. * * Also returns an ApiProblemResponse in cases of: * * - Invalid input filter service name * - Missing ParameterDataContainer (i.e., ContentNegotiation is not registered) * * @param MvcEvent $e * @return null|ApiProblemResponse */ public function onRoute(MvcEvent $e) { $request = $e->getRequest(); if (!$request instanceof HttpRequest) { return; } $routeMatches = $e->getRouteMatch(); if (!($routeMatches instanceof RouteMatch || $routeMatches instanceof V2RouteMatch)) { return; } $controllerService = $routeMatches->getParam('controller', false); if (!$controllerService) { return; } $method = $request->getMethod(); $inputFilterService = $this->getInputFilterService($controllerService, $method); if (!$inputFilterService) { return; } if (!$this->hasInputFilter($inputFilterService)) { return new ApiProblemResponse(new ApiProblem(500, sprintf('Listed input filter "%s" does not exist; cannot validate request', $inputFilterService))); } $dataContainer = $e->getParam('ZFContentNegotiationParameterData', false); if (!$dataContainer instanceof ParameterDataContainer) { return new ApiProblemResponse(new ApiProblem(500, 'ZF\\ContentNegotiation module is not initialized; cannot validate request')); } $data = in_array($method, $this->methodsWithoutBodies) ? $dataContainer->getQueryParams() : $dataContainer->getBodyParams(); if (null === $data || '' === $data) { $data = []; } $isCollection = $this->isCollection($controllerService, $data, $routeMatches, $request); $files = $request->getFiles(); if (!$isCollection && 0 < count($files)) { // File uploads are not validated for collections; impossible to // match file fields to discrete sets $data = ArrayUtils::merge($data, $files->toArray(), true); } $inputFilter = $this->getInputFilter($inputFilterService); if ($isCollection && !in_array($method, $this->methodsWithoutBodies)) { $collectionInputFilter = new CollectionInputFilter(); $collectionInputFilter->setInputFilter($inputFilter); $inputFilter = $collectionInputFilter; } $e->setParam('ZF\\ContentValidation\\InputFilter', $inputFilter); $currentEventName = $e->getName(); $e->setName(self::EVENT_BEFORE_VALIDATE); $events = $this->getEventManager(); $results = $events->triggerEventUntil(function ($result) { return $result instanceof ApiProblem || $result instanceof ApiProblemResponse; }, $e); $e->setName($currentEventName); $last = $results->last(); if ($last instanceof ApiProblem) { $last = new ApiProblemResponse($last); } if ($last instanceof ApiProblemResponse) { return $last; } $inputFilter->setData($data); $status = $request->isPatch() ? $this->validatePatch($inputFilter, $data, $isCollection) : $inputFilter->isValid(); if ($status instanceof ApiProblemResponse) { return $status; } // Invalid? Return a 422 response. if (false === $status) { return new ApiProblemResponse(new ApiProblem(422, 'Failed Validation', null, null, ['validation_messages' => $inputFilter->getMessages()])); } // Should we use the raw data vs. the filtered data? // - If no `use_raw_data` flag is present, always use the raw data, as // that was the default experience starting in 1.0. // - If the flag is present AND is boolean true, that is also // an indicator that the raw data should be present. $useRawData = $this->useRawData($controllerService); if (!$useRawData) { $data = $inputFilter->getValues(); } // If we don't have an instance of UnknownInputsCapableInterface, or no // unknown data is in the input filter, at this point we can just // set the current data into the data container. if (!$inputFilter instanceof UnknownInputsCapableInterface || !$inputFilter->hasUnknown()) { $dataContainer->setBodyParams($data); return; } $unknown = $inputFilter->getUnknown(); if ($this->allowsOnlyFieldsInFilter($controllerService)) { if ($inputFilter instanceof CollectionInputFilter) { $unknownFields = []; foreach ($unknown as $key => $fields) { $unknownFields[] = '[' . $key . ': ' . implode(', ', array_keys($fields)) . ']'; } $fields = implode(', ', $unknownFields); } else { $fields = implode(', ', array_keys($unknown)); } $detail = sprintf('Unrecognized fields: %s', $fields); $problem = new ApiProblem(Response::STATUS_CODE_422, $detail); return new ApiProblemResponse($problem); } // The raw data already contains unknown inputs, so no need to merge // them with the data. if ($useRawData) { $dataContainer->setBodyParams($data); return; } // When not using raw data, we merge the unknown data with the // validated data to get the full set of input. $dataContainer->setBodyParams(array_merge($data, $unknown)); }
/** * Complete the request * * Triggers "render" and "finish" events, and returns response from * event object. * * @param MvcEvent $event * @return Application */ protected function completeRequest(MvcEvent $event) { $events = $this->events; $event->setTarget($this); $event->setName(MvcEvent::EVENT_RENDER); $event->stopPropagation(false); // Clear before triggering $events->triggerEvent($event); $event->setName(MvcEvent::EVENT_FINISH); $event->stopPropagation(false); // Clear before triggering $events->triggerEvent($event); return $this; }