/** * Checks access for the account and route using the custom access checker. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The route match object to be checked. * @param \Drupal\Core\Session\AccountInterface $account * The account being checked. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) { $callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access')); $arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account); $arguments = $arguments_resolver->getArguments($callable); return call_user_func_array($callable, $arguments); }
/** * {@inheritdoc} */ public function getTitle(Request $request, Route $route) { $route_title = NULL; // A dynamic title takes priority. Route::getDefault() returns NULL if the // named default is not set. By testing the value directly, we also avoid // trying to use empty values. if ($callback = $route->getDefault('_title_callback')) { $callable = $this->controllerResolver->getControllerFromDefinition($callback); $arguments = $this->controllerResolver->getArguments($request, $callable); $route_title = call_user_func_array($callable, $arguments); } elseif ($title = $route->getDefault('_title')) { $options = array(); if ($context = $route->getDefault('_title_context')) { $options['context'] = $context; } $args = array(); if ($raw_parameters = $request->attributes->get('_raw_variables')) { foreach ($raw_parameters->all() as $key => $value) { $args['@' . $key] = $value; $args['%' . $key] = $value; } } if ($title_arguments = $route->getDefault('_title_arguments')) { $args = array_merge($args, (array) $title_arguments); } // Fall back to a static string from the route. $route_title = $this->t($title, $args, $options); } return $route_title; }
/** * Returns the result of invoking the sub-controller. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param mixed $controller_definition * A controller definition string, or a callable object/closure. * * @return mixed * The result of invoking the controller. Render arrays, strings, HtmlPage, * and HtmlFragment objects are possible. */ public function getContentResult(Request $request, $controller_definition) { if ($controller_definition instanceof \Closure) { $callable = $controller_definition; } else { $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition); } $arguments = $this->controllerResolver->getArguments($request, $callable); $page_content = call_user_func_array($callable, $arguments); return $page_content; }
/** * {@inheritdoc} */ public function rebuild() { if ($this->building) { throw new \RuntimeException('Recursive router rebuild detected.'); } if (!$this->lock->acquire('router_rebuild')) { // Wait for another request that is already doing this work. // We choose to block here since otherwise the routes might not be // available, resulting in a 404. $this->lock->wait('router_rebuild'); return FALSE; } $this->building = TRUE; $collection = new RouteCollection(); foreach ($this->getRouteDefinitions() as $routes) { // The top-level 'routes_callback' is a list of methods in controller // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods // should return a set of \Symfony\Component\Routing\Route objects, either // in an associative array keyed by the route name, which will be iterated // over and added to the collection for this provider, or as a new // \Symfony\Component\Routing\RouteCollection object, which will be added // to the collection. if (isset($routes['route_callbacks'])) { foreach ($routes['route_callbacks'] as $route_callback) { $callback = $this->controllerResolver->getControllerFromDefinition($route_callback); if ($callback_routes = call_user_func($callback)) { // If a RouteCollection is returned, add the whole collection. if ($callback_routes instanceof RouteCollection) { $collection->addCollection($callback_routes); } else { foreach ($callback_routes as $name => $callback_route) { $collection->add($name, $callback_route); } } } } unset($routes['route_callbacks']); } foreach ($routes as $name => $route_info) { $route_info += array('defaults' => array(), 'requirements' => array(), 'options' => array()); $route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options']); $collection->add($name, $route); } } // DYNAMIC is supposed to be used to add new routes based upon all the // static defined ones. $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection)); // ALTER is the final step to alter all the existing routes. We cannot stop // people from adding new routes here, but we define two separate steps to // make it clear. $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection)); $this->checkProvider->setChecks($collection); $this->dumper->addRoutes($collection); $this->dumper->dump(); $this->lock->release('router_rebuild'); $this->dispatcher->dispatch(RoutingEvents::FINISHED, new Event()); $this->building = FALSE; $this->rebuildNeeded = FALSE; return TRUE; }
/** * Creates a controller instance using route defaults. * * By design we cannot support all possible routes, but just the ones which * use the defaults provided by core, which are _content, _controller * and _form. * * @param array $defaults * The default values provided by the route. * * @return array|null * Returns the controller instance if it is possible to instantiate it, NULL */ protected function getController(array $defaults) { $controller = NULL; if (isset($defaults['_content'])) { $controller = $this->controllerResolver->getControllerFromDefinition($defaults['_content']); } if (isset($defaults['_controller'])) { $controller = $this->controllerResolver->getControllerFromDefinition($defaults['_controller']); } if (isset($defaults['_form'])) { $form_arg = $defaults['_form']; // Check if the class exists first as the class resolver will throw an // exception if it doesn't. This also means a service cannot be used here. if (class_exists($form_arg)) { $controller = array($this->classResolver->getInstanceFromDefinition($form_arg), 'buildForm'); } } return $controller; }
/** * {@inheritdoc} */ public function generateCachePlaceholder($callback, array &$context) { if (is_string($callback) && strpos($callback, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callback); } else { $callable = $callback; } if (!is_callable($callable)) { throw new \InvalidArgumentException('$callable must be a callable function or of the form service_id:method.'); } // Generate a unique token if one is not already provided. $context += ['token' => Crypt::randomBytesBase64(55)]; return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>'; }
/** * {@inheritdoc} */ public function transform(array $tree, array $manipulators) { foreach ($manipulators as $manipulator) { $callable = $manipulator['callable']; $callable = $this->controllerResolver->getControllerFromDefinition($callable); // Prepare the arguments for the menu tree manipulator callable; the first // argument is always the menu link tree. if (isset($manipulator['args'])) { array_unshift($manipulator['args'], $tree); $tree = call_user_func_array($callable, $manipulator['args']); } else { $tree = call_user_func($callable, $tree); } } return $tree; }
/** * Builds all permissions provided by .permissions.yml files. * * @return array[] * Each return permission is an array with the following keys: * - title: The title of the permission. * - description: The description of the permission, defaults to NULL. * - provider: The provider of the permission. */ protected function buildPermissionsYaml() { $all_permissions = array(); $all_callback_permissions = array(); foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) { // The top-level 'permissions_callback' is a list of methods in controller // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods // should return an array of permissions in the same structure. if (isset($permissions['permission_callbacks'])) { foreach ($permissions['permission_callbacks'] as $permission_callback) { $callback = $this->controllerResolver->getControllerFromDefinition($permission_callback); if ($callback_permissions = call_user_func($callback)) { // Add any callback permissions to the array of permissions. Any // defaults can then get processed below. foreach ($callback_permissions as $name => $callback_permission) { if (!is_array($callback_permission)) { $callback_permission = array('title' => $callback_permission); } $callback_permission += array('description' => NULL); $callback_permission['provider'] = $provider; $all_callback_permissions[$name] = $callback_permission; } } } unset($permissions['permission_callbacks']); } foreach ($permissions as &$permission) { if (!is_array($permission)) { $permission = array('title' => $permission); } $permission['title'] = $this->t($permission['title']); $permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL; $permission['provider'] = $provider; } $all_permissions += $permissions; } return $all_permissions + $all_callback_permissions; }
/** * See the docs for ::render(). */ protected function doRender(&$elements, $is_root_call = FALSE) { if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); } // Early-return nothing if user does not have access. if (empty($elements) || isset($elements['#access']) && !$elements['#access']) { return ''; } // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; } if (!isset(static::$stack)) { static::$stack = new \SplStack(); } static::$stack->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. // - this is a render cacheable subtree, to ensure that the cached data has // the configurable defaults (which may affect the ID and invalidation). if ($is_root_call || isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } else { $elements['#cache']['contexts'] = $required_cache_contexts; } } // Try to fetch the prerendered element from cache, replace any placeholders // and return the final markup. if (isset($elements['#cache']['keys'])) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache // in case of nested elements with #cache set. if ($is_root_call) { $this->replacePlaceholders($elements); } // Mark the element markup as safe. If we have cached children, we need // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the // individual children must be safe as well. $elements['#markup'] = SafeMarkup::set($elements['#markup']); if (!empty($elements['#cache_properties'])) { foreach (Element::children($cached_element) as $key) { SafeMarkup::set($cached_element[$key]['#markup']); } } // The render cache item contains all the bubbleable rendering metadata // for the subtree. $this->updateStack($elements); // Render cache hit, so rendering is finished, all necessary info // collected! $this->bubbleStack(); return $elements['#markup']; } } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. // @see \Drupal\Core\Render\RenderCacheInterface::get() // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { $elements += $this->elementInfo->getInfo($elements['#type']); } // First validate the usage of #lazy_builder; both of the next if-statements // use it if available. if (isset($elements['#lazy_builder'])) { // @todo Convert to assertions once https://www.drupal.org/node/2408013 // lands. if (!is_array($elements['#lazy_builder'])) { throw new \DomainException('The #lazy_builder property must have an array as a value.'); } if (count($elements['#lazy_builder']) !== 2) { throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.'); } if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) { return is_null($v) || is_scalar($v); }))) { throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL."); } $children = Element::children($elements); if ($children) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children))); } $supported_keys = ['#lazy_builder', '#cache', '#create_placeholder', '#weight', '#printed']; $unsupported_keys = array_diff(array_keys($elements), $supported_keys); if (count($unsupported_keys)) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys))); } } // If instructed to create a placeholder, and a #lazy_builder callback is // present (without such a callback, it would be impossible to replace the // placeholder), replace the current element with a placeholder. if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) { if (!isset($elements['#lazy_builder'])) { throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.'); } $elements = $this->createPlaceholder($elements); } // Build the element if it is still empty. if (isset($elements['#lazy_builder'])) { $callable = $elements['#lazy_builder'][0]; $args = $elements['#lazy_builder'][1]; if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $new_elements = call_user_func_array($callable, $args); // Retain the original cacheability metadata, plus cache keys. CacheableMetadata::createFromRenderArray($elements)->merge(CacheableMetadata::createFromRenderArray($new_elements))->applyTo($new_elements); if (isset($elements['#cache']['keys'])) { $new_elements['#cache']['keys'] = $elements['#cache']['keys']; } $elements = $new_elements; $elements['#lazy_builder_built'] = TRUE; } // Make any final changes to the element before it is rendered. This means // that the $element or the children can be altered or corrected before the // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements = call_user_func($callable, $elements); } } // Defaults for bubbleable rendering metadata. $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. $this->updateStack($elements); // #printed, so rendering is finished, all necessary info collected! $this->bubbleStack(); return ''; } // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { drupal_process_states($elements); } // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE); // Initialize this element's #children, unless a #pre_render callback // already preset #children. if (!isset($elements['#children'])) { $elements['#children'] = ''; } if (isset($elements['#markup'])) { // @todo Decide how to support non-HTML in the render API in // https://www.drupal.org/node/2501313. $elements['#markup'] = SafeMarkup::checkAdminXss($elements['#markup']); } // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); // Check the elements for insecure HTML and pass through sanitization. if (isset($elements)) { $markup_keys = array('#description', '#field_prefix', '#field_suffix'); foreach ($markup_keys as $key) { if (!empty($elements[$key]) && is_scalar($elements[$key])) { $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); } } } // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite // recursion. if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the // hook in #theme was not found in the registry and so we need to update // our flag accordingly. This is common for theme suggestions. $theme_is_implemented = $elements['#children'] !== FALSE; } // If #theme is not implemented or #render_children is set and the element // has an empty #children attribute, render the children now. This is the // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } $elements['#children'] = SafeMarkup::set($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a // fallback, prepend the content in #markup to #children. In this case // #children will contain whatever is provided by #pre_render prepended to // what is rendered recursively above. If #theme is implemented then it is // the responsibility of that theme implementation to render #markup if // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered // children. // #states and #attached have to be processed before #theme_wrappers, // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. // If the internal #render_children property is set, do not call the // #theme_wrappers function(s) to prevent infinite recursion. if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute // overrides. Attribute overrides replace key/value pairs in $elements // for only this ThemeManagerInterface::render() call. This allows // #theme hooks and #theme_wrappers hooks to share variable names // without conflict or ambiguity. $wrapper_elements = $elements; if (is_string($key)) { $wrapper_hook = $key; foreach ($value as $attribute => $override) { $wrapper_elements[$attribute] = $override; } } else { $wrapper_hook = $value; } $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); } } // Filter the outputted content and make any last changes before the content // is sent to the browser. The changes are made on $content which allows the // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); } } // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that placeholder // replacement logic gets the same data to work with, no matter if #cache is // disabled, #cache is enabled, there is a cache hit or miss. $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; $elements['#markup'] = $prefix . $elements['#children'] . $suffix; // We've rendered this element (and its subtree!), now update the stack. $this->updateStack($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } $this->renderCache->set($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache in // case of nested elements with #cache set. // // By running them here, we ensure that: // - they run when #cache is disabled, // - they run when #cache is enabled and there is a cache miss. // Only the case of a cache hit when #cache is enabled, is not handled here, // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); if (static::$stack->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! $this->bubbleStack(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); return $elements['#markup']; }