protected function loadRoutes() { $this->routes->add('dynamic_route_' . ($this->routes->count() + 1), new Route('pup/image/upload/service', $defaults = array('_controller' => 'pupBundle:Process:upload'), $requirements = array())); //add another //or execute a db query and add multiple routes //etc. }
/** * {@inheritdoc} */ public function read(ReflectionClass $class, array $options = []) { if ($class->isAbstract()) { throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class)); } $options = array_replace(['path' => null, 'name' => null, 'requirements' => [], 'options' => [], 'defaults' => []], $options); if ($annotation = $this->getAnnotationReader()->getClassAnnotation($class, $this->routeAnnotation)) { if ($annotation->getPath() !== null) { $options['path'] = $annotation->getPath(); } if ($annotation->getName() !== null) { $options['name'] = $annotation->getName(); } if ($annotation->getRequirements() !== null) { $options['requirements'] = $annotation->getRequirements(); } if ($annotation->getOptions() !== null) { $options['options'] = $annotation->getOptions(); } if ($annotation->getDefaults() !== null) { $options['defaults'] = $annotation->getDefaults(); } } if ($options['path'] === null) { $options['path'] = strtolower($this->parseControllerName($class)); } if ($options['name'] === null) { $options['name'] = '@' . strtolower($this->parseControllerName($class)); } $this->routes = new RouteCollection(); foreach ($class->getMethods() as $method) { $this->routeIndex = 0; if ($method->isPublic() && 'Action' == substr($method->name, -6)) { $count = $this->routes->count(); foreach ($this->getAnnotationReader()->getMethodAnnotations($method) as $annotation) { if ($annotation instanceof $this->routeAnnotation) { $this->addRoute($class, $method, $options, $annotation); } } if ($count == $this->routes->count()) { $this->addRoute($class, $method, $options); } } } return $this->routes; }
/** * Find a list of controllers * * @param string $base_path Base path to prepend to file paths * @return router */ public function find($base_path = '') { if ($this->route_collection === null || $this->route_collection->count() === 0) { $this->route_collection = new RouteCollection(); foreach ($this->routing_files as $file_path) { $loader = new YamlFileLoader(new FileLocator($this->filesystem->realpath($base_path))); $this->route_collection->addCollection($loader->load($file_path)); } } return $this; }
/** * {@inheritdoc} * * Invalid page manager routes will be removed. Routes not controlled by page * manager will be moved to the end of the collection. Once a valid page * manager route has been found, all other page manager routes will also be * removed. */ public function filter(RouteCollection $collection, Request $request) { // Only proceed if the collection is non-empty. if (!$collection->count()) { return $collection; } // Store the unaltered request attributes. $original_attributes = $request->attributes->all(); // First get all routes and sort them by variant weight. Note that routes // without a weight will have an undefined order, they are ignored here. $routes = $collection->all(); uasort($routes, [$this, 'routeWeightSort']); // Find the first route that is accessible. $accessible_route_name = NULL; foreach ($routes as $name => $route) { $attributes = $this->getRequestAttributes($route, $name, $request); // Add the enhanced attributes to the request. $request->attributes->add($attributes); if ($page_variant_id = $route->getDefault('page_manager_page_variant')) { if ($this->checkPageVariantAccess($page_variant_id)) { // Access granted, use this route. Do not restore request attributes // but keep those from this route by breaking out. $accessible_route_name = $name; break; } } // Restore the original request attributes, this must be done in the loop // or the request attributes will not be calculated correctly for the // next route. $request->attributes->replace($original_attributes); } // Because the sort order of $routes is unreliable for a route without a // variant weight, rely on the original order of $collection here. foreach ($collection as $name => $route) { if ($route->getDefault('page_manager_page_variant')) { if ($accessible_route_name !== $name) { // Remove all other page manager routes. $collection->remove($name); } } else { // This is not page manager route, move it to the end of the collection, // those will only be used if there is no accessible variant route. $collection->add($name, $route); } } return $collection; }
/** * {@inheritdoc} * * Invalid page manager routes will be removed. Routes not controlled by page * manager will be moved to the end of the collection. Once a valid page * manager route has been found, all other page manager routes will also be * removed. */ public function filter(RouteCollection $collection, Request $request) { // Only proceed if the collection is non-empty. if (!$collection->count()) { return $collection; } // Store the unaltered request attributes. $original_attributes = $request->attributes->all(); $page_manager_route_found = FALSE; foreach ($collection as $name => $route) { $attributes = $this->getRequestAttributes($route, $name, $request); // Add the enhanced attributes to the request. $request->attributes->add($attributes); if ($page_variant_id = $route->getDefault('page_manager_page_variant')) { // If a page manager route was already found, remove this one from the // collection. if ($page_manager_route_found) { $collection->remove($name); } elseif ($this->checkPageVariantAccess($page_variant_id)) { // Mark that a valid page manager route was found. $page_manager_route_found = TRUE; // Replace the original attributes with the newly processed attributes. $original_attributes = $request->attributes->all(); } else { // Remove routes for variants that fail access. $collection->remove($name); } } else { // If this route has no page variant, move it to the end of the list. $collection->add($name, $route); } // Restore the original request attributes. $request->attributes->replace($original_attributes); } return $collection; }
/** * @return string */ protected function createRouteIndex() { return (string) sprintf('_s_mantle_redirect_%\'.04d', (int) $this->routeCollection->count()); }
/** * Tests overriding an existing route. * * @covers ::alterRoutes * @covers ::findPageRouteName * * @dataProvider providerTestAlterRoutesOverrideExisting */ public function testAlterRoutesOverrideExisting($page_path, $existing_route_path, $requirements = []) { $route_name = 'test_route'; // Set up a page with the same path as an existing route. $page = $this->prophesize(PageInterface::class); $page->status()->willReturn(TRUE)->shouldBeCalled(); $page->getPath()->willReturn($page_path)->shouldBeCalled(); $variant1 = $this->prophesize(PageVariantInterface::class); $variant1->getWeight()->willReturn(0); $page->getVariants()->willReturn(['variant1' => $variant1->reveal()]); $page->id()->willReturn('page1'); $page->label()->willReturn(NULL); $page->usesAdminTheme()->willReturn(FALSE); $this->pageStorage->loadMultiple()->willReturn(['page1' => $page->reveal()])->shouldBeCalledTimes(1); $this->cacheTagsInvalidator->invalidateTags(["page_manager_route_name:{$route_name}"])->shouldBeCalledTimes(1); $collection = new RouteCollection(); $collection->add($route_name, new Route($existing_route_path, ['default_exists' => 'default_value'], $requirements, ['parameters' => ['foo' => 'bar']])); $route_event = new RouteBuildEvent($collection); $this->routeSubscriber->onAlterRoutes($route_event); // The normal route name is not used, the existing route name is instead. $this->assertSame(1, $collection->count()); $this->assertNull($collection->get('page_manager.page_view_page1')); $this->assertNull($collection->get('page_manager.page_view_page1_variant1')); $route = $collection->get($route_name); $expected_defaults = ['_entity_view' => 'page_manager_page_variant', '_title' => NULL, 'page_manager_page_variant' => 'variant1', 'page_manager_page' => 'page1', 'page_manager_page_variant_weight' => 0, 'base_route_name' => $route_name]; $expected_requirements = $requirements; $expected_options = ['compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', 'parameters' => ['page_manager_page_variant' => ['type' => 'entity:page_variant'], 'page_manager_page' => ['type' => 'entity:page'], 'foo' => 'bar'], '_admin_route' => FALSE]; $this->assertMatchingRoute($route, $existing_route_path, $expected_defaults, $expected_requirements, $expected_options); }
/** * Find the matching route and execute the controller * * Algorithm - http://www.minutephp.com/ADD * # Get the URL and Method from the event * Find the matching route by URL and method * Get the controller, auth and list of models from route * Throw error if the user is not authorized to access the route * * If Method is GET * Parse the models to create parent child relations * Throw error if the user isn't allowed to Read the model * Create a list of constraints using \ * \ conditions in the route model definitions * \ conditions added using `addConstraint` method * Perform additional checks if permission is SAME_USER * If the controller is null guess it from the route URL * If this is an ajax request with metadata then override the controller with our own GET handler * Call the controller with the constraints using an event * * If Method is POST * If this is an ajax request and we have model definitions * Parse the models in the route * Get the model, and items and type of operation from the request * Throw error if the user cannot the operation on selected model * Create models instances for each item * Call the GenericPostHandler with the instances using an event * end. * * @param RequestEvent $requestEvent * * @throws AuthError * @throws ModelError * @throws RouteError * @throws ValidationError */ public function handle(RequestEvent $requestEvent) { $request = $requestEvent->getRequest(); //try { $parents = null; $method = $request->getMethod(); $this->compile(); //Since we can have multiple POST requests for a single Url if ($method === 'POST' && ($alias = $request->getParameter('alias')) && ($class = $request->getParameter('model'))) { $newCollection = new RouteCollection(); foreach (['alias' => $alias, 'name' => $class] as $key => $value) { /** @var RouteEx $routeEx */ foreach ($this->routeCollection as $name => $routeEx) { if (in_array('POST', $routeEx->getMethods())) { $parents = $routeEx->parsePostModels(); if ($filtered = array_filter($parents, function ($m) use($key, $value) { return $m[$key] === $value; })) { $newCollection->add($name, $routeEx); } } } } if ($newCollection->count()) { $this->routeCollection = $newCollection; } else { throw new ResourceNotFoundException("No Post handler configured for alias '{$alias}' or class '{$class}'"); } } try { $route = $this->match($method, $request->getPath()); } catch (ResourceNotFoundException $e) { $event = new RouterEvent($method, $request->getPath()); $this->dispatcher->fire(RouterEvent::ROUTER_GET_FALLBACK_RESOURCE, $event); if (!($route = $event->getRoute())) { throw $e; } } $event = new AuthEvent($route->getDefault('auth')); $this->lastMatchingRoute = $route; $this->dispatcher->fire(AuthEvent::AUTH_CHECK_ACCESS, $event); if ($event->isAuthorized()) { $user_id = $event->getActiveUserId(); $controller = $controllerArgs = null; $contentType = 'html'; if ($method === 'POST') { $parents = $route->parsePostModels(); $controller = $route->getDefault('controller'); $matches = []; if (empty($controller)) { if (!empty($parents)) { $controller = 'Generic/DefaultPostHandler.php'; } else { $controller = trim(Str::camel(str_replace(' ', '', ucwords(preg_replace('/(\\W+)/', '\\1 ', $route->getPath())))), '/'); } } if (!empty($parents)) { if ($alias = $request->getParameter('alias')) { $matches = array_filter($parents, function ($f) use($alias) { return $f['alias'] === $alias; }); } if (count($matches) === 1) { $model = array_shift($matches); } else { if ($class = $request->getParameter('model')) { $matches = array_filter($parents, function ($f) use($class) { return $f['name'] === $class; }); } if (count($matches) === 1) { $model = array_shift($matches); } else { throw new ModelError("Post route does not have a matching model for alias:'{$alias}' or class:'{$class}'"); } } /** @var ModelEx $inst */ if ($modelClass = $this->resolver->getModel($model['name'])) { $inst = new $modelClass(); $cmd = $request->getParameter('cmd', 'save'); $pri = $inst->getKeyName(); $items = $request->getParameter('items', []); $cols = $this->database->getColumns($inst->getTable()); if (!empty($items)) { foreach ((array) $items as $item) { $accessMode = $cmd == 'save' ? !empty($item[$pri]) ? 'update' : 'create' : 'delete'; $permission = call_user_func([$route, sprintf('get%sPermission', ucwords($accessMode))], $model['alias']); if ($this->canAccessModel($permission, $event->isLoggedInUser())) { $fields = $model['fields'] ?? $cols; $immutable = [$pri, 'user_id']; $fields = array_diff($fields, $immutable); $has_uid = in_array('user_id', $cols); if (count(array_diff(array_keys($item), array_merge($fields, $immutable))) !== 0) { throw new ValidationError(sprintf("Field restriction on '%s' failed on fields: '%s'", $model['alias'], join(', ', array_keys($item)))); } if ($accessMode === 'create') { /** @var ModelEx $m */ $modelClass::unguard(); $instance = new $modelClass($item); } else { if ($instance = $modelClass::where($pri, '=', $item[$pri])->first()) { if ($accessMode !== 'delete') { foreach ($fields as $field) { $instance->{$field} = $item[$field] ?? $instance->{$field} ?? null; } } } else { throw new ModelError(sprintf("No record found in %s for '%s' = '%s'", $model['name'], $pri, $item[$pri])); } } if ($permission == Permission::SAME_USER) { if (!$instance->user_id) { $instance->user_id = $user_id; } else { if ($instance->user_id !== $user_id) { throw new ModelError(sprintf("%s user_id does not match to current user.", ucfirst($model['name']))); } } } if ($has_uid && $accessMode !== 'delete') { $instance->user_id = $instance->user_id ?: ($user_id ?: ($item['user_id'] ?: 0)); } $models[] = $instance; } else { throw new ModelError(sprintf("%s access denied to '%s' in %s. Enable auth or change read permission", ucwords($accessMode), $model['alias'], $route->getPath())); } } } } else { throw new RouteError(sprintf("Cannot create model for class: %s in %s", $model['name'], $route->getPath())); } } $controllerArgs = ['_parents' => $parents, '_models' => $models ?? null, '_mode' => $accessMode ?? $cmd ?? 'update']; } elseif ($method === 'GET') { $parents = $route->parseGetModels(); $metadata = $request->getParameter('metadata'); $addConstraints = function ($alias, $permission, $self) use($route, $event, $user_id) { if ($canIgnore = $permission === SpecialPermission::SAME_USER_OR_IGNORE) { $permission = Permission::SAME_USER; } $hasAccess = $this->canAccessModel($permission, $event->isLoggedInUser()); if (!$hasAccess && $canIgnore) { $route->addConstraint($alias, function (Builder $builder) { return $builder->whereRaw('1 = 0'); }); } elseif ($hasAccess) { $colValue = null; if ($permission === Permission::SAME_USER) { $route->addConstraint($alias, ['user_id', '=', $user_id]); } if ($matchInfo = $self['matchInfo'] ?? null) { if (!empty($matchInfo['name'])) { list($name, $col, $type) = [$matchInfo['name'], $matchInfo['col'], $matchInfo['type']]; $colValue = $type === 'url_param' ? $route->getDefault($name) : ($type === 'var' ? ${$name} : ($type === 'string' ? $matchInfo['value'] : null)); } } if (!empty($col) && isset($colValue)) { $route->addConstraint($alias, [$col, '=', $colValue]); } elseif ($route->getDefault($alias) !== '*' && $permission !== Permission::SAME_USER) { //we block all queries when there is no matchInfo (unless alias explicitly defaults to *) $route->addConstraint($alias, function (Builder $builder) { return $builder->whereRaw('1 = 0'); }); } } else { throw new ModelError(sprintf("Read access denied to %s in %s. Enable auth or change read permission", $alias, $route->getPath())); } }; $joiner = function ($parents) use(&$joiner, $route, $addConstraints) { foreach ($parents['children'] as $alias => $value) { $permission = $route->getJoinPermission($alias); if ($permission !== Permission::EVERYONE) { $addConstraints($alias, $permission, $value['self']); } if (!empty($value['children'])) { $joiner($value); } } }; foreach ($parents as $key => $value) { $self = $value['self']; if ($parentModelClass = $this->resolver->getModel($self['name'])) { $parentAlias = $self['alias']; $permission = $route->getReadPermission($parentAlias); $addConstraints($parentAlias, $permission, $self); } else { throw new RouteError(sprintf("Cannot create model for class: %s in %s", $self['name'], $route->getPath())); } $joiner($value); } $controller = $route->getDefault('controller'); $modifyNodeByAlias = function (&$root, $alias, $modifiers, $append) use(&$modifyNodeByAlias) { foreach ($root as $parent => $child) { if ($parent === $alias) { foreach ($modifiers as $key => $value) { if ($append) { //conditions $root[$parent]['self'][$key] = array_merge($root[$parent]['self'][$key] ?? [], $value); } else { //offset, limit, etc $root[$parent]['self'][$key] = $value; } } return true; } elseif (!empty($child['children']) && ($found = $modifyNodeByAlias($root[$parent]['children'], $alias, $modifiers, $append))) { return $found; } } return false; }; foreach ($route->getAllConstraints() as $alias => $constraint) { if (!$modifyNodeByAlias($parents, $alias, ['conditions' => $constraint], true)) { throw new ModelError("Could not find model {$alias} to add condition"); } } if ($controller === null) { $controller = 'Generic/Page.php'; } if ($request->isAjaxRequest() && !empty($metadata)) { //we override controller for ajax request which have $_GET['metadata'] set $metadata = json_decode($metadata, true); $contentType = 'ajax'; foreach ($metadata ?? [] as $alias => $values) { if (!$modifyNodeByAlias($parents, $alias, $values, false)) { throw new ModelError("Could not find model {$alias} to apply metadata"); } } } $controllerArgs = ['_parents' => $parents]; } $defaults = array_diff_key($route->getDefaults(), array_flip(['url', 'controller', 'auth', 'models', '_route'])); $params = array_merge($defaults, $request->getParameters()); $event = new ControllerEvent($controller, array_merge(['_route' => $route, '_method' => $method, '_params' => $params, '_contentType' => $contentType], $params, $controllerArgs ?? [])); $this->dispatcher->fire(ControllerEvent::CONTROLLER_EXECUTE, $event); $response = $event->getResponse(); } else { throw new AuthError('Forbidden'); } $requestEvent->setResponse($response); }
/** * Tests overriding an existing route. * * @covers ::alterRoutes */ public function testAlterRoutesOverrideExisting() { // Set up a page with the same path as an existing route. $page = $this->prophesize(PageInterface::class); $page->status()->willReturn(TRUE)->shouldBeCalledTimes(1); $page->getPath()->willReturn('/test_route')->shouldBeCalledTimes(1); $page->isFallbackPage()->willReturn(FALSE); $page->label()->willReturn(NULL); $page->usesAdminTheme()->willReturn(FALSE); $this->pageStorage->loadMultiple()->willReturn(['page1' => $page->reveal()])->shouldBeCalledTimes(1); $collection = new RouteCollection(); $collection->add('test_route', new Route('test_route', [], [], ['parameters' => ['foo' => 'bar']])); $route_event = new RouteBuildEvent($collection); $this->routeSubscriber->onAlterRoutes($route_event); // The normal route name is not used, the existing route name is instead. $this->assertSame(1, $collection->count()); $this->assertNull($collection->get('page_manager.page_view_page1')); $route = $collection->get('test_route'); $expected_defaults = ['_entity_view' => 'page_manager_page', 'page_manager_page' => 'page1', '_title' => NULL]; $expected_requirements = ['_entity_access' => 'page_manager_page.view']; $expected_options = ['compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', 'parameters' => ['page_manager_page' => ['type' => 'entity:page'], 'foo' => 'bar'], '_admin_route' => FALSE]; $this->assertMatchingRoute($route, '/test_route', $expected_defaults, $expected_requirements, $expected_options); }
/** * @covers ::alterRoutes */ public function testAlterRoutesMultipleVariantsDifferentRequirements() { $variant1 = $this->prophesize(PageVariantInterface::class); $variant2 = $this->prophesize(PageVariantInterface::class); $variant1->getWeight()->willReturn(0); $page1 = $this->prophesize(PageInterface::class); $page1->status()->willReturn(TRUE); $page1->getVariants()->willReturn(['variant1' => $variant1->reveal()]); $page1->getPath()->willReturn('/test_route1'); $page1->getParameters()->willReturn([]); $page1->id()->willReturn('page1'); $page1->label()->willReturn('Page 1'); $page1->usesAdminTheme()->willReturn(FALSE); $page2 = $this->prophesize(PageInterface::class); $page2->status()->willReturn(TRUE); $page2->getVariants()->willReturn(['variant2' => $variant2->reveal()]); $page2->getPath()->willReturn('/test_route2'); $page2->getParameters()->willReturn([]); $page2->id()->willReturn('page2'); $page2->label()->willReturn('Page 2'); $page2->usesAdminTheme()->willReturn(FALSE); $this->pageStorage->loadMultiple()->willReturn(['page1' => $page1->reveal(), 'page2' => $page2->reveal()]); $collection = new RouteCollection(); $collection->add('test_route', new Route('/test_route1', [], ['_access' => 'TRUE'], [])); $route_event = new RouteBuildEvent($collection); $this->routeSubscriber->onAlterRoutes($route_event); $this->assertSame(2, $collection->count()); $expected = ['test_route' => ['path' => '/test_route1', 'defaults' => ['_entity_view' => 'page_manager_page_variant', '_title' => 'Page 1', 'page_manager_page_variant' => 'variant1', 'page_manager_page' => 'page1', 'page_manager_page_variant_weight' => 0, 'base_route_name' => 'test_route'], 'requirements' => ['_access' => 'TRUE', '_page_access' => 'page_manager_page.view'], 'options' => ['compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', 'parameters' => ['page_manager_page_variant' => ['type' => 'entity:page_variant'], 'page_manager_page' => ['type' => 'entity:page']], '_admin_route' => FALSE]], 'page_manager.page_view_page2' => ['path' => '/test_route2', 'defaults' => ['_entity_view' => 'page_manager_page_variant', '_title' => 'Page 2', 'page_manager_page_variant' => 'variant2', 'page_manager_page' => 'page2', 'page_manager_page_variant_weight' => 0, 'base_route_name' => 'page_manager.page_view_page2'], 'requirements' => ['_page_access' => 'page_manager_page.view'], 'options' => ['compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', 'parameters' => ['page_manager_page_variant' => ['type' => 'entity:page_variant'], 'page_manager_page' => ['type' => 'entity:page']], '_admin_route' => FALSE]]]; foreach ($collection as $route_name => $route) { $this->assertMatchingRoute($route, $expected[$route_name]['path'], $expected[$route_name]['defaults'], $expected[$route_name]['requirements'], $expected[$route_name]['options']); } }