Issue #2351015 by plach, effulgentsia, Wim Leers, dawehner, martin107, damiankloip, cilefen, Fabianx, catch, pwolanin, Damien Tournoud, znerol, YesCT, larowlan: URL generation does not bubble cache contexts
parent
ae32aaae81
commit
20f1c993b6
|
@ -694,9 +694,15 @@ services:
|
|||
arguments: ['@route_filter.lazy_collector']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
url_generator:
|
||||
url_generator.non_bubbling:
|
||||
class: Drupal\Core\Routing\UrlGenerator
|
||||
arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@request_stack']
|
||||
public: false
|
||||
calls:
|
||||
- [setContext, ['@?router.request_context']]
|
||||
url_generator:
|
||||
class: Drupal\Core\Render\MetadataBubblingUrlGenerator
|
||||
arguments: ['@url_generator.non_bubbling', '@renderer']
|
||||
calls:
|
||||
- [setContext, ['@?router.request_context']]
|
||||
redirect.destination:
|
||||
|
@ -1425,7 +1431,7 @@ services:
|
|||
arguments: ['@request_stack', '@cache_factory', '@cache_contexts_manager']
|
||||
renderer:
|
||||
class: Drupal\Core\Render\Renderer
|
||||
arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%']
|
||||
arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '@request_stack', '%renderer.config%']
|
||||
early_rendering_controller_wrapper_subscriber:
|
||||
class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber
|
||||
arguments: ['@controller_resolver', '@renderer']
|
||||
|
|
|
@ -165,7 +165,7 @@ function _batch_progress_page() {
|
|||
$query_options['op'] = $new_op;
|
||||
$batch['url']->setOption('query', $query_options);
|
||||
|
||||
$url = $batch['url']->toString();
|
||||
$url = $batch['url']->toString(TRUE)->getGeneratedUrl();
|
||||
|
||||
$build = array(
|
||||
'#theme' => 'progress_bar',
|
||||
|
|
|
@ -810,7 +810,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
|
|||
$query_options['op'] = 'finished';
|
||||
$error_url->setOption('query', $query_options);
|
||||
|
||||
$batch['error_message'] = t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => $error_url->toString()));
|
||||
$batch['error_message'] = t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => $error_url->toString(TRUE)->getGeneratedUrl()));
|
||||
|
||||
// Clear the way for the redirection to the batch processing page, by
|
||||
// saving and unsetting the 'destination', if there is any.
|
||||
|
@ -840,7 +840,7 @@ function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = N
|
|||
$function($batch_url->toString(), ['query' => $query_options]);
|
||||
}
|
||||
else {
|
||||
return new RedirectResponse($batch_url->setAbsolute()->toString());
|
||||
return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -176,6 +176,7 @@ function template_preprocess_pager(&$variables) {
|
|||
$element = $variables['pager']['#element'];
|
||||
$parameters = $variables['pager']['#parameters'];
|
||||
$quantity = $variables['pager']['#quantity'];
|
||||
$route_name = $variables['pager']['#route_name'];
|
||||
global $pager_page_array, $pager_total;
|
||||
|
||||
// Nothing to do if there is only one page.
|
||||
|
@ -218,7 +219,7 @@ function template_preprocess_pager(&$variables) {
|
|||
$options = array(
|
||||
'query' => pager_query_add_page($parameters, $element, 0),
|
||||
);
|
||||
$items['first']['href'] = \Drupal::url('<current>', [], $options);
|
||||
$items['first']['href'] = \Drupal::url($route_name, [], $options);
|
||||
if (isset($tags[0])) {
|
||||
$items['first']['text'] = $tags[0];
|
||||
}
|
||||
|
@ -227,7 +228,7 @@ function template_preprocess_pager(&$variables) {
|
|||
$options = array(
|
||||
'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] - 1),
|
||||
);
|
||||
$items['previous']['href'] = \Drupal::url('<current>', [], $options);
|
||||
$items['previous']['href'] = \Drupal::url($route_name, [], $options);
|
||||
if (isset($tags[1])) {
|
||||
$items['previous']['text'] = $tags[1];
|
||||
}
|
||||
|
@ -243,7 +244,7 @@ function template_preprocess_pager(&$variables) {
|
|||
$options = array(
|
||||
'query' => pager_query_add_page($parameters, $element, $i - 1),
|
||||
);
|
||||
$items['pages'][$i]['href'] = \Drupal::url('<current>', [], $options);
|
||||
$items['pages'][$i]['href'] = \Drupal::url($route_name, [], $options);
|
||||
if ($i == $pager_current) {
|
||||
$variables['current'] = $i;
|
||||
}
|
||||
|
@ -260,7 +261,7 @@ function template_preprocess_pager(&$variables) {
|
|||
$options = array(
|
||||
'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] + 1),
|
||||
);
|
||||
$items['next']['href'] = \Drupal::url('<current>', [], $options);
|
||||
$items['next']['href'] = \Drupal::url($route_name, [], $options);
|
||||
if (isset($tags[3])) {
|
||||
$items['next']['text'] = $tags[3];
|
||||
}
|
||||
|
@ -269,13 +270,18 @@ function template_preprocess_pager(&$variables) {
|
|||
$options = array(
|
||||
'query' => pager_query_add_page($parameters, $element, $pager_max - 1),
|
||||
);
|
||||
$items['last']['href'] = \Drupal::url('<current>', [], $options);
|
||||
$items['last']['href'] = \Drupal::url($route_name, [], $options);
|
||||
if (isset($tags[4])) {
|
||||
$items['last']['text'] = $tags[4];
|
||||
}
|
||||
}
|
||||
|
||||
$variables['items'] = $items;
|
||||
|
||||
// The rendered link needs to play well with any other query parameter
|
||||
// used on the page, like exposed filters, so for the cacheability all query
|
||||
// parameters matter.
|
||||
$variables['#cache']['contexts'][] = 'url.query_args';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
namespace Drupal\Core\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Cache\CacheableResponseInterface;
|
||||
use Drupal\Core\Controller\ControllerResolverInterface;
|
||||
|
@ -14,6 +15,7 @@ use Drupal\Core\Render\AttachmentsInterface;
|
|||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
@ -44,10 +46,12 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
|||
* metadata is then merged onto the render array.
|
||||
*
|
||||
* In other words: this just exists to ease the transition to Drupal 8: it
|
||||
* allows controllers that return render arrays (the majority) to still do early
|
||||
* rendering. But controllers that return responses are already expected to do
|
||||
* the right thing: if early rendering is detected in such a case, an exception
|
||||
* is thrown.
|
||||
* allows controllers that return render arrays (the majority) and
|
||||
* \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
|
||||
* often involve a fair amount of rendering) to still do early rendering. But
|
||||
* controllers that return any other kind of response are already expected to
|
||||
* do the right thing, so if early rendering is detected in such a case, an
|
||||
* exception is thrown.
|
||||
*
|
||||
* @see \Drupal\Core\Render\RendererInterface
|
||||
* @see \Drupal\Core\Render\Renderer
|
||||
|
@ -129,15 +133,26 @@ class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterf
|
|||
// drupal_render() outside of a render context, then the bubbleable metadata
|
||||
// for that is stored in the current render context.
|
||||
if (!$context->isEmpty()) {
|
||||
// If a render array is returned by the controller, merge the "lost"
|
||||
// bubbleable metadata.
|
||||
/** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
|
||||
$early_rendering_bubbleable_metadata = $context->pop();
|
||||
|
||||
// If a render array or AjaxResponse is returned by the controller, merge
|
||||
// the "lost" bubbleable metadata.
|
||||
if (is_array($response)) {
|
||||
$early_rendering_bubbleable_metadata = $context->pop();
|
||||
BubbleableMetadata::createFromRenderArray($response)
|
||||
->merge($early_rendering_bubbleable_metadata)
|
||||
->applyTo($response);
|
||||
}
|
||||
// If a Response or domain object is returned, and it cares about
|
||||
elseif ($response instanceof AjaxResponse) {
|
||||
$response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
|
||||
// @todo Make AjaxResponse cacheable in
|
||||
// https://www.drupal.org/node/956186. Meanwhile, allow contrib
|
||||
// subclasses to be.
|
||||
if ($response instanceof CacheableResponseInterface) {
|
||||
$response->addCacheableDependency($early_rendering_bubbleable_metadata);
|
||||
}
|
||||
}
|
||||
// If a non-Ajax Response or domain object is returned and it cares about
|
||||
// attachments or cacheability, then throw an exception: early rendering
|
||||
// is not permitted in that case. It is the developer's responsibility
|
||||
// to not use early rendering.
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
namespace Drupal\Core\Render\Element;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides a base class for form element plugins.
|
||||
|
@ -111,18 +113,29 @@ abstract class FormElement extends RenderElement implements FormElementInterface
|
|||
* The form element.
|
||||
*/
|
||||
public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) {
|
||||
$url = NULL;
|
||||
$access = FALSE;
|
||||
|
||||
if (!empty($element['#autocomplete_route_name'])) {
|
||||
$parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
|
||||
|
||||
$path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
|
||||
$access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
|
||||
$url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE);
|
||||
/** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
|
||||
$access_manager = \Drupal::service('access_manager');
|
||||
$access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE);
|
||||
}
|
||||
|
||||
if ($access) {
|
||||
$element['#attributes']['class'][] = 'form-autocomplete';
|
||||
$element['#attached']['library'][] = 'core/drupal.autocomplete';
|
||||
// Provide a data attribute for the JavaScript behavior to bind to.
|
||||
$element['#attributes']['data-autocomplete-path'] = $path;
|
||||
$metadata = BubbleableMetadata::createFromRenderArray($element);
|
||||
if ($access->isAllowed()) {
|
||||
$element['#attributes']['class'][] = 'form-autocomplete';
|
||||
$element['#attached']['library'][] = 'core/drupal.autocomplete';
|
||||
// Provide a data attribute for the JavaScript behavior to bind to.
|
||||
$element['#attributes']['data-autocomplete-path'] = $url->getGeneratedUrl();
|
||||
$metadata->merge($url);
|
||||
}
|
||||
$metadata
|
||||
->merge(BubbleableMetadata::createFromObject($access))
|
||||
->applyTo($element);
|
||||
}
|
||||
|
||||
return $element;
|
||||
|
|
|
@ -34,18 +34,29 @@ class Pager extends RenderElement{
|
|||
'#quantity' => 9,
|
||||
// An array of labels for the controls in the pager.
|
||||
'#tags' => [],
|
||||
// The name of the route to be used to build pager links. By default no
|
||||
// path is provided, which will make links relative to the current URL.
|
||||
// This makes the page more effectively cacheable.
|
||||
'#route_name' => '<none>',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* #pre_render callback to associate the appropriate cache context.
|
||||
*
|
||||
*
|
||||
* @param array $pager
|
||||
* A renderable array of #type => pager.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function preRenderPager(array $pager) {
|
||||
// Note: the default pager theme process function
|
||||
// template_preprocess_pager() also calls pager_query_add_page(), which
|
||||
// maintains the existing query string. Therefore
|
||||
// template_preprocess_pager() adds the 'url.query_args' cache context,
|
||||
// which causes the more specific cache context below to be optimized away.
|
||||
// In other themes, however, that may not be the case.
|
||||
$pager['#cache']['contexts'][] = 'url.query_args.pagers:' . $pager['#element'];
|
||||
return $pager;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace Drupal\Core\Render\Element;
|
|||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
|
@ -253,7 +254,11 @@ abstract class RenderElement extends PluginBase implements ElementInterface {
|
|||
|
||||
// Convert \Drupal\Core\Url object to string.
|
||||
if (isset($settings['url']) && $settings['url'] instanceof Url) {
|
||||
$settings['url'] = $settings['url']->setOptions($settings['options'])->toString();
|
||||
$url = $settings['url']->setOptions($settings['options'])->toString(TRUE);
|
||||
BubbleableMetadata::createFromRenderArray($element)
|
||||
->merge($url)
|
||||
->applyTo($element);
|
||||
$settings['url'] = $url->getGeneratedUrl();
|
||||
}
|
||||
else {
|
||||
$settings['url'] = NULL;
|
||||
|
|
|
@ -36,6 +36,7 @@ class HtmlResponse extends Response implements CacheableResponseInterface, Attac
|
|||
// A render array can automatically be converted to a string and set the
|
||||
// necessary metadata.
|
||||
if (is_array($content) && (isset($content['#markup']))) {
|
||||
$content += ['#attached' => ['html_response_placeholders' => []]];
|
||||
$this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
|
||||
$this->setAttachments($content['#attached']);
|
||||
$content = $content['#markup'];
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Render\MetadataBubblingUrlGenerator.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Core\GeneratedUrl;
|
||||
use Drupal\Core\Routing\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
|
||||
|
||||
/**
|
||||
* Decorator for the URL generator, which bubbles bubbleable URL metadata.
|
||||
*
|
||||
* Implements a decorator for the URL generator that allows to automatically
|
||||
* collect and bubble up bubbleable metadata associated with URLs due to
|
||||
* outbound path and route processing. This approach helps keeping the render
|
||||
* and the routing subsystems decoupled.
|
||||
*
|
||||
* @see \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface
|
||||
* @see \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
|
||||
* @see \Drupal\Core\Render\BubbleableMetadata
|
||||
*/
|
||||
class MetadataBubblingUrlGenerator implements UrlGeneratorInterface {
|
||||
|
||||
/**
|
||||
* The non-bubbling URL generator.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\UrlGeneratorInterface
|
||||
*/
|
||||
protected $urlGenerator;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a new bubbling URL generator service.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
|
||||
* The non-bubbling URL generator.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer.
|
||||
*/
|
||||
public function __construct(UrlGeneratorInterface $url_generator, RendererInterface $renderer) {
|
||||
$this->urlGenerator = $url_generator;
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setContext(SymfonyRequestContext $context) {
|
||||
$this->urlGenerator->setContext($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContext() {
|
||||
return $this->urlGenerator->getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPathFromRoute($name, $parameters = array()) {
|
||||
return $this->urlGenerator->getPathFromRoute($name, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubbles the bubbleable metadata to the current render context.
|
||||
*
|
||||
* @param \Drupal\Core\GeneratedUrl $generated_url
|
||||
* The generated URL whose bubbleable metadata to bubble.
|
||||
* @param array $options
|
||||
* (optional) The URL options. Defaults to none.
|
||||
*/
|
||||
protected function bubble(GeneratedUrl $generated_url, array $options = []) {
|
||||
// Bubbling metadata makes sense only if the code is executed inside a
|
||||
// render context. All code running outside controllers has no render
|
||||
// context by default, so URLs used there are not supposed to affect the
|
||||
// response cacheability.
|
||||
if ($this->renderer->hasRenderContext()) {
|
||||
$build = [];
|
||||
$generated_url->applyTo($build);
|
||||
$this->renderer->render($build);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate($name, $parameters = array(), $absolute = FALSE) {
|
||||
$options['absolute'] = $absolute;
|
||||
$generated_url = $this->generateFromRoute($name, $parameters, $options, TRUE);
|
||||
$this->bubble($generated_url);
|
||||
return $generated_url->getGeneratedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
|
||||
$generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
|
||||
if (!$collect_bubbleable_metadata) {
|
||||
$this->bubble($generated_url, $options);
|
||||
}
|
||||
return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generateFromPath($path = NULL, $options = array(), $collect_bubbleable_metadata = FALSE) {
|
||||
$generated_url = $this->urlGenerator->generateFromPath($path, $options, TRUE);
|
||||
if (!$collect_bubbleable_metadata) {
|
||||
$this->bubble($generated_url, $options);
|
||||
}
|
||||
return $collect_bubbleable_metadata ? $generated_url : $generated_url->getGeneratedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports($name) {
|
||||
return $this->urlGenerator->supports($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRouteDebugMessage($name, array $parameters = array()) {
|
||||
return $this->urlGenerator->getRouteDebugMessage($name, $parameters);
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
|
@ -16,6 +15,7 @@ use Drupal\Core\Cache\CacheableMetadata;
|
|||
use Drupal\Core\Controller\ControllerResolverInterface;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Turns a render array into a HTML string.
|
||||
|
@ -58,7 +58,25 @@ class Renderer implements RendererInterface {
|
|||
protected $rendererConfig;
|
||||
|
||||
/**
|
||||
* The render context.
|
||||
* Whether we're currently in a ::renderRoot() call.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isRenderingRoot = FALSE;
|
||||
|
||||
/**
|
||||
* The request stack.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\RequestStack
|
||||
*/
|
||||
protected $requestStack;
|
||||
|
||||
/**
|
||||
* The render context collection.
|
||||
*
|
||||
* An individual global render context is tied to the current request. We then
|
||||
* need to maintain a different context for each request to correctly handle
|
||||
* rendering in subrequests.
|
||||
*
|
||||
* This must be static as long as some controllers rebuild the container
|
||||
* during a request. This causes multiple renderer instances to co-exist
|
||||
|
@ -66,16 +84,9 @@ class Renderer implements RendererInterface {
|
|||
* fail to render correctly. As soon as it is guaranteed that during a request
|
||||
* the same container is used, it no longer needs to be static.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RenderContext|null
|
||||
* @var \Drupal\Core\Render\RenderContext[]
|
||||
*/
|
||||
protected static $context;
|
||||
|
||||
/**
|
||||
* Whether we're currently in a ::renderRoot() call.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isRenderingRoot = FALSE;
|
||||
protected static $contextCollection;
|
||||
|
||||
/**
|
||||
* Constructs a new Renderer.
|
||||
|
@ -88,15 +99,23 @@ class Renderer implements RendererInterface {
|
|||
* The element info.
|
||||
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
|
||||
* The render cache service.
|
||||
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
|
||||
* The request stack.
|
||||
* @param array $renderer_config
|
||||
* The renderer configuration array.
|
||||
*/
|
||||
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, array $renderer_config) {
|
||||
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
|
||||
$this->controllerResolver = $controller_resolver;
|
||||
$this->theme = $theme;
|
||||
$this->elementInfo = $element_info;
|
||||
$this->renderCache = $render_cache;
|
||||
$this->rendererConfig = $renderer_config;
|
||||
$this->requestStack = $request_stack;
|
||||
|
||||
// Initialize the context collection if needed.
|
||||
if (!isset(static::$contextCollection)) {
|
||||
static::$contextCollection = new \SplObjectStorage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -225,10 +244,11 @@ class Renderer implements RendererInterface {
|
|||
return '';
|
||||
}
|
||||
|
||||
if (!isset(static::$context)) {
|
||||
$context = $this->getCurrentRenderContext();
|
||||
if (!isset($context)) {
|
||||
throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
|
||||
}
|
||||
static::$context->push(new BubbleableMetadata());
|
||||
$context->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
|
||||
|
@ -269,10 +289,10 @@ class Renderer implements RendererInterface {
|
|||
}
|
||||
// The render cache item contains all the bubbleable rendering metadata
|
||||
// for the subtree.
|
||||
static::$context->update($elements);
|
||||
$context->update($elements);
|
||||
// Render cache hit, so rendering is finished, all necessary info
|
||||
// collected!
|
||||
static::$context->bubble();
|
||||
$context->bubble();
|
||||
return $elements['#markup'];
|
||||
}
|
||||
}
|
||||
|
@ -370,9 +390,9 @@ class Renderer implements RendererInterface {
|
|||
if (!empty($elements['#printed'])) {
|
||||
// The #printed element contains all the bubbleable rendering metadata for
|
||||
// the subtree.
|
||||
static::$context->update($elements);
|
||||
$context->update($elements);
|
||||
// #printed, so rendering is finished, all necessary info collected!
|
||||
static::$context->bubble();
|
||||
$context->bubble();
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -499,7 +519,7 @@ class Renderer implements RendererInterface {
|
|||
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
|
||||
|
||||
// We've rendered this element (and its subtree!), now update the context.
|
||||
static::$context->update($elements);
|
||||
$context->update($elements);
|
||||
|
||||
// Cache the processed element if both $pre_bubbling_elements and $elements
|
||||
// have the metadata necessary to generate a cache ID.
|
||||
|
@ -522,40 +542,73 @@ class Renderer implements RendererInterface {
|
|||
if ($is_root_call) {
|
||||
$this->replacePlaceholders($elements);
|
||||
// @todo remove as part of https://www.drupal.org/node/2511330.
|
||||
if (static::$context->count() !== 1) {
|
||||
if ($context->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!
|
||||
static::$context->bubble();
|
||||
$context->bubble();
|
||||
|
||||
$elements['#printed'] = TRUE;
|
||||
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
|
||||
return $elements['#markup'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasRenderContext() {
|
||||
return (bool) $this->getCurrentRenderContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeInRenderContext(RenderContext $context, callable $callable) {
|
||||
// Store the current render context.
|
||||
$current_context = static::$context;
|
||||
$previous_context = $this->getCurrentRenderContext();
|
||||
|
||||
// Set the provided context and call the callable, it will use that context.
|
||||
static::$context = $context;
|
||||
$this->setCurrentRenderContext($context);
|
||||
$result = $callable();
|
||||
// @todo Convert to an assertion in https://www.drupal.org/node/2408013
|
||||
if (static::$context->count() > 1) {
|
||||
if ($context->count() > 1) {
|
||||
throw new \LogicException('Bubbling failed.');
|
||||
}
|
||||
|
||||
// Restore the original render context.
|
||||
static::$context = $current_context;
|
||||
$this->setCurrentRenderContext($previous_context);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current render context.
|
||||
*
|
||||
* @return \Drupal\Core\Render\RenderContext
|
||||
* The current render context.
|
||||
*/
|
||||
protected function getCurrentRenderContext() {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current render context.
|
||||
*
|
||||
* @param \Drupal\Core\Render\RenderContext|null $context
|
||||
* The render context. This can be NULL for instance when restoring the
|
||||
* original render context, which is in fact NULL.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setCurrentRenderContext(RenderContext $context = NULL) {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
static::$contextCollection[$request] = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces placeholders.
|
||||
*
|
||||
|
|
|
@ -321,6 +321,18 @@ interface RendererInterface {
|
|||
*/
|
||||
public function render(&$elements, $is_root_call = FALSE);
|
||||
|
||||
/**
|
||||
* Checks whether a render context is active.
|
||||
*
|
||||
* This is useful only in very specific situations to determine whether the
|
||||
* system is already capable of collecting bubbleable metadata. Normally it
|
||||
* should not be necessary to be concerned about this.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the renderer has a render context active, FALSE otherwise.
|
||||
*/
|
||||
public function hasRenderContext();
|
||||
|
||||
/**
|
||||
* Executes a callable within a render context.
|
||||
*
|
||||
|
|
|
@ -278,12 +278,9 @@ class UrlGenerator implements UrlGeneratorInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
|
||||
$generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
|
||||
|
||||
$options += array('prefix' => '');
|
||||
$route = $this->getRoute($name);
|
||||
$name = $this->getRouteDebugMessage($name);
|
||||
$this->processRoute($name, $route, $parameters, $generated_url);
|
||||
$generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
|
||||
|
||||
$query_params = [];
|
||||
// Symfony adds any parameters that are not path slugs as query strings.
|
||||
|
@ -291,6 +288,23 @@ class UrlGenerator implements UrlGeneratorInterface {
|
|||
$query_params = $options['query'];
|
||||
}
|
||||
|
||||
$fragment = '';
|
||||
if (isset($options['fragment'])) {
|
||||
if (($fragment = trim($options['fragment'])) != '') {
|
||||
$fragment = '#' . $fragment;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a relative URL having no path, just query string and fragment.
|
||||
if ($route->getOption('_no_path')) {
|
||||
$query = $query_params ? '?' . http_build_query($query_params, '', '&') : '';
|
||||
$url = $query . $fragment;
|
||||
return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
|
||||
}
|
||||
|
||||
$options += array('prefix' => '');
|
||||
$name = $this->getRouteDebugMessage($name);
|
||||
$this->processRoute($name, $route, $parameters, $generated_url);
|
||||
$path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
|
||||
$path = $this->processPath($path, $options, $generated_url);
|
||||
|
||||
|
@ -300,13 +314,6 @@ class UrlGenerator implements UrlGeneratorInterface {
|
|||
$path = '/' . str_replace('%2F', '/', rawurlencode($prefix)) . $path;
|
||||
}
|
||||
|
||||
$fragment = '';
|
||||
if (isset($options['fragment'])) {
|
||||
if (($fragment = trim($options['fragment'])) != '') {
|
||||
$fragment = '#' . $fragment;
|
||||
}
|
||||
}
|
||||
|
||||
// The base_url might be rewritten from the language rewrite in domain mode.
|
||||
if (isset($options['base_url'])) {
|
||||
$base_url = $options['base_url'];
|
||||
|
@ -328,11 +335,6 @@ class UrlGenerator implements UrlGeneratorInterface {
|
|||
|
||||
$absolute = !empty($options['absolute']);
|
||||
if (!$absolute || !$host = $this->context->getHost()) {
|
||||
|
||||
if ($route->getOption('_only_fragment')) {
|
||||
return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($fragment) : $fragment;
|
||||
}
|
||||
|
||||
$url = $base_url . $path . $fragment;
|
||||
return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
|
||||
}
|
||||
|
|
|
@ -302,14 +302,23 @@ class ThemeManager implements ThemeManagerInterface {
|
|||
$preprocessor_function($variables, $hook, $info);
|
||||
}
|
||||
}
|
||||
// Allow theme preprocess functions to set $variables['#attached'] and use
|
||||
// it like the #attached property on render arrays. In Drupal 8, this is
|
||||
// the (only) officially supported method of attaching assets from
|
||||
// preprocess functions. Assets attached here should be associated with
|
||||
// the template that we're preprocessing variables for.
|
||||
if (isset($variables['#attached'])) {
|
||||
$preprocess_attached = ['#attached' => $variables['#attached']];
|
||||
drupal_render($preprocess_attached);
|
||||
// Allow theme preprocess functions to set $variables['#attached'] and
|
||||
// $variables['#cache'] and use them like the corresponding element
|
||||
// properties on render arrays. In Drupal 8, this is the (only) officially
|
||||
// supported method of attaching bubbleable metadata from preprocess
|
||||
// functions. Assets attached here should be associated with the template
|
||||
// that we are preprocessing variables for.
|
||||
$preprocess_bubbleable = [];
|
||||
foreach (['#attached', '#cache'] as $key) {
|
||||
if (isset($variables[$key])) {
|
||||
$preprocess_bubbleable[$key] = $variables[$key];
|
||||
}
|
||||
}
|
||||
// We do not allow preprocess functions to define cacheable elements.
|
||||
unset($preprocess_bubbleable['#cache']['keys']);
|
||||
if ($preprocess_bubbleable) {
|
||||
// @todo Inject the Renderer in https://www.drupal.org/node/2529438.
|
||||
drupal_render($preprocess_bubbleable);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ use Drupal\comment\CommentInterface;
|
|||
use Drupal\comment\CommentManagerInterface;
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Cache\CacheableResponseInterface;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
|
@ -129,7 +130,8 @@ class CommentController extends ControllerBase {
|
|||
// Find the current display page for this comment.
|
||||
$page = $this->entityManager()->getStorage('comment')->getDisplayOrdinal($comment, $field_definition->getSetting('default_mode'), $field_definition->getSetting('per_page'));
|
||||
// @todo: Cleaner sub request handling.
|
||||
$redirect_request = Request::create($entity->url(), 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
|
||||
$subrequest_url = $entity->urlInfo()->toString(TRUE);
|
||||
$redirect_request = Request::create($subrequest_url->getGeneratedUrl(), 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
|
||||
$redirect_request->query->set('page', $page);
|
||||
// Carry over the session to the subrequest.
|
||||
if ($session = $request->getSession()) {
|
||||
|
@ -137,7 +139,16 @@ class CommentController extends ControllerBase {
|
|||
}
|
||||
// @todo: Convert the pager to use the request object.
|
||||
$request->query->set('page', $page);
|
||||
return $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
|
||||
$response = $this->httpKernel->handle($redirect_request, HttpKernelInterface::SUB_REQUEST);
|
||||
if ($response instanceof CacheableResponseInterface) {
|
||||
// @todo Once path aliases have cache tags (see
|
||||
// https://www.drupal.org/node/2480077), add test coverage that
|
||||
// the cache tag for a commented entity's path alias is added to the
|
||||
// comment's permalink response, because there can be blocks or
|
||||
// other content whose renderings depend on the subrequest's URL.
|
||||
$response->addCacheableDependency($subrequest_url);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ class CommentRssTest extends CommentTestBase {
|
|||
$cache_contexts = [
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'url.site',
|
||||
'user.node_grants:view',
|
||||
'user.permissions',
|
||||
'timezone',
|
||||
|
|
|
@ -192,12 +192,17 @@ class TextFormat extends RenderElement {
|
|||
'#parents' => array_merge($element['#parents'], array('format')),
|
||||
);
|
||||
|
||||
$element['format']['help'] = array(
|
||||
$element['format']['help'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => array('class' => array('filter-help')),
|
||||
'#markup' => \Drupal::l(t('About text formats'), new Url('filter.tips_all', array(), array('attributes' => array('target' => '_blank')))),
|
||||
'about' => [
|
||||
'#type' => 'link',
|
||||
'#title' => t('About text formats'),
|
||||
'#url' => new Url('filter.tips_all'),
|
||||
'#attributes' => ['target' => '_blank'],
|
||||
],
|
||||
'#attributes' => ['class' => ['filter-help']],
|
||||
'#weight' => 0,
|
||||
);
|
||||
];
|
||||
|
||||
$all_formats = filter_formats();
|
||||
$format_exists = isset($all_formats[$element['#format']]);
|
||||
|
|
|
@ -139,7 +139,7 @@ class LanguageUrlRewritingTest extends WebTestBase {
|
|||
|
||||
// Create an absolute French link.
|
||||
$language = \Drupal::languageManager()->getLanguage('fr');
|
||||
$url = Url::fromRoute('<none>', [], [
|
||||
$url = Url::fromRoute('<front>', [], [
|
||||
'absolute' => TRUE,
|
||||
'language' => $language,
|
||||
])->toString();
|
||||
|
@ -149,7 +149,7 @@ class LanguageUrlRewritingTest extends WebTestBase {
|
|||
$this->assertEqual($url, $expected, 'The right port is used.');
|
||||
|
||||
// If we set the port explicitly, it should not be overridden.
|
||||
$url = Url::fromRoute('<none>', [], [
|
||||
$url = Url::fromRoute('<front>', [], [
|
||||
'absolute' => TRUE,
|
||||
'language' => $language,
|
||||
'base_url' => $request->getBaseUrl() . ':90',
|
||||
|
|
|
@ -191,7 +191,7 @@ class FrontPageTest extends ViewTestBase {
|
|||
*/
|
||||
public function testCacheTagsWithCachePluginNone() {
|
||||
$this->enablePageCaching();
|
||||
$this->assertFrontPageViewCacheTags(FALSE);
|
||||
$this->doTestFrontPageViewCacheTags(FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -207,7 +207,7 @@ class FrontPageTest extends ViewTestBase {
|
|||
]);
|
||||
$view->save();
|
||||
|
||||
$this->assertFrontPageViewCacheTags(TRUE);
|
||||
$this->doTestFrontPageViewCacheTags(TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -227,7 +227,7 @@ class FrontPageTest extends ViewTestBase {
|
|||
]);
|
||||
$view->save();
|
||||
|
||||
$this->assertFrontPageViewCacheTags(TRUE);
|
||||
$this->doTestFrontPageViewCacheTags(TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,7 +236,7 @@ class FrontPageTest extends ViewTestBase {
|
|||
* @param bool $do_assert_views_caches
|
||||
* Whether to check Views' result & output caches.
|
||||
*/
|
||||
protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
|
||||
protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
|
||||
$view = Views::getView('frontpage');
|
||||
$view->setDisplay('page_1');
|
||||
|
||||
|
@ -248,7 +248,9 @@ class FrontPageTest extends ViewTestBase {
|
|||
'user.permissions',
|
||||
// Default cache contexts of the renderer.
|
||||
'theme',
|
||||
'url.query_args.pagers:0',
|
||||
'url.query_args',
|
||||
// Attached feed.
|
||||
'url.site',
|
||||
];
|
||||
|
||||
$cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
|
||||
|
|
|
@ -71,12 +71,7 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
|
|||
|
||||
$cache_contexts = [
|
||||
'languages:' . LanguageInterface::TYPE_INTERFACE,
|
||||
'route.menu_active_trails:account',
|
||||
'route.menu_active_trails:footer',
|
||||
'route.menu_active_trails:main',
|
||||
'route.menu_active_trails:tools',
|
||||
// The user login block access is not visible on certain routes.
|
||||
'route.name',
|
||||
'route',
|
||||
'theme',
|
||||
'timezone',
|
||||
'user.permissions',
|
||||
|
|
|
@ -109,7 +109,10 @@ class EntityResource extends ResourceBase {
|
|||
$this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
|
||||
|
||||
// 201 Created responses have an empty body.
|
||||
return new ResourceResponse(NULL, 201, array('Location' => $entity->url('canonical', ['absolute' => TRUE])));
|
||||
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
|
||||
$response = new ResourceResponse(NULL, 201, ['Location' => $url->getGeneratedUrl()]);
|
||||
$response->addCacheableDependency($url);
|
||||
return $response;
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
throw new HttpException(500, 'Internal Server Error', $e);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
namespace Drupal\rest;
|
||||
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
|
@ -103,10 +104,23 @@ class RequestHandler implements ContainerAwareInterface {
|
|||
}
|
||||
|
||||
// Serialize the outgoing data for the response, if available.
|
||||
$data = $response->getResponseData();
|
||||
if ($data != NULL) {
|
||||
$output = $serializer->serialize($data, $format);
|
||||
if ($response instanceof ResourceResponse && $data = $response->getResponseData()) {
|
||||
// Serialization can invoke rendering (e.g., generating URLs), but the
|
||||
// serialization API does not provide a mechanism to collect the
|
||||
// bubbleable metadata associated with that (e.g., language and other
|
||||
// contexts), so instead, allow those to "leak" and collect them here in
|
||||
// a render context.
|
||||
// @todo Add test coverage for language negotiation contexts in
|
||||
// https://www.drupal.org/node/2135829.
|
||||
$context = new RenderContext();
|
||||
$output = $this->container->get('renderer')->executeInRenderContext($context, function() use ($serializer, $data, $format) {
|
||||
return $serializer->serialize($data, $format);
|
||||
});
|
||||
$response->setContent($output);
|
||||
if (!$context->isEmpty()) {
|
||||
$response->addCacheableDependency($context->pop());
|
||||
}
|
||||
|
||||
$response->headers->set('Content-Type', $request->getMimeType($format));
|
||||
// Add rest settings config's cache tags.
|
||||
$response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
|
||||
|
|
|
@ -313,7 +313,6 @@ function shortcut_preprocess_page(&$variables) {
|
|||
'link' => $link,
|
||||
'name' => $variables['title'],
|
||||
);
|
||||
$query += \Drupal::destination()->getAsArray();
|
||||
|
||||
$shortcut_set = shortcut_current_displayed_set();
|
||||
|
||||
|
@ -341,6 +340,7 @@ function shortcut_preprocess_page(&$variables) {
|
|||
}
|
||||
|
||||
if (theme_get_setting('third_party_settings.shortcut.module_link')) {
|
||||
$query += \Drupal::destination()->getAsArray();
|
||||
$variables['title_suffix']['add_or_remove_shortcut'] = array(
|
||||
'#attached' => array(
|
||||
'library' => array(
|
||||
|
|
|
@ -21,7 +21,7 @@ class ShortcutTranslationUITest extends ContentTranslationUITestBase {
|
|||
/**
|
||||
* {inheritdoc}
|
||||
*/
|
||||
protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user'];
|
||||
protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'user', 'url.site'];
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
|
|
|
@ -32,6 +32,7 @@ use Drupal\Core\Url;
|
|||
use Drupal\node\Entity\NodeType;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
||||
/**
|
||||
* Test case for typical Drupal tests.
|
||||
|
@ -2393,16 +2394,35 @@ abstract class WebTestBase extends TestBase {
|
|||
/**
|
||||
* Takes a path and returns an absolute path.
|
||||
*
|
||||
* @param $path
|
||||
* This method is implemented in the way that browsers work, see
|
||||
* https://url.spec.whatwg.org/#relative-state for more information about the
|
||||
* possible cases.
|
||||
*
|
||||
* @param string $path
|
||||
* A path from the internal browser content.
|
||||
*
|
||||
* @return
|
||||
* @return string
|
||||
* The $path with $base_url prepended, if necessary.
|
||||
*/
|
||||
protected function getAbsoluteUrl($path) {
|
||||
global $base_url, $base_path;
|
||||
|
||||
$parts = parse_url($path);
|
||||
|
||||
// In case the $path has a host, it is already an absolute URL and we are
|
||||
// done.
|
||||
if (!empty($parts['host'])) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
// In case the $path contains just a query, we turn it into an absolute URL
|
||||
// with the same scheme, host and path, see
|
||||
// https://url.spec.whatwg.org/#relative-state.
|
||||
if (array_keys($parts) === ['query']) {
|
||||
$current_uri = new Uri($this->getUrl());
|
||||
return (string) $current_uri->withQuery($parts['query']);
|
||||
}
|
||||
|
||||
if (empty($parts['host'])) {
|
||||
// Ensure that we have a string (and no xpath object).
|
||||
$path = (string) $path;
|
||||
|
@ -2860,6 +2880,17 @@ abstract class WebTestBase extends TestBase {
|
|||
$this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a cache context was not present in the last response.
|
||||
*
|
||||
* @param string $not_expected_cache_context
|
||||
* The expected cache context.
|
||||
*/
|
||||
protected function assertNoCacheContext($not_expected_cache_context) {
|
||||
$cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
|
||||
$this->assertFalse(in_array($not_expected_cache_context, $cache_contexts), "'" . $not_expected_cache_context . "' is not present in the X-Drupal-Cache-Contexts header.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts whether an expected cache tag was present in the last response.
|
||||
*
|
||||
|
|
|
@ -198,4 +198,41 @@ class WebTestBaseTest extends UnitTestCase {
|
|||
$this->assertSame($expected, $clicklink_method->invoke($web_test, $label, $index));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerTestGetAbsoluteUrl
|
||||
*/
|
||||
public function testGetAbsoluteUrl($href, $expected_absolute_path) {
|
||||
$web_test = $this->getMockBuilder('Drupal\simpletest\WebTestBase')
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(['getUrl'])
|
||||
->getMock();
|
||||
|
||||
$web_test->expects($this->any())
|
||||
->method('getUrl')
|
||||
->willReturn('http://example.com/drupal/current-path?foo=baz');
|
||||
|
||||
$GLOBALS['base_url'] = 'http://example.com';
|
||||
$GLOBALS['base_path'] = 'drupal';
|
||||
|
||||
$get_absolute_url_method = new \ReflectionMethod($web_test, 'getAbsoluteUrl');
|
||||
$get_absolute_url_method->setAccessible(TRUE);
|
||||
|
||||
$this->assertSame($expected_absolute_path, $get_absolute_url_method->invoke($web_test, $href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for testGetAbsoluteUrl.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function providerTestGetAbsoluteUrl() {
|
||||
$data = [];
|
||||
$data['host'] = ['http://example.com/drupal/test-example', 'http://example.com/drupal/test-example'];
|
||||
$data['path'] = ['/drupal/test-example', 'http://example.com/drupal/test-example'];
|
||||
$data['path-with-query'] = ['/drupal/test-example?foo=bar', 'http://example.com/drupal/test-example?foo=bar'];
|
||||
$data['just-query'] = ['?foo=bar', 'http://example.com/drupal/current-path?foo=bar'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ class DbUpdateController extends ControllerBase {
|
|||
|
||||
$info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.");
|
||||
$info[] = $this->t('Put your site into <a href="@url">maintenance mode</a>.', array(
|
||||
'@url' => $this->url('system.site_maintenance_mode'),
|
||||
'@url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
|
||||
));
|
||||
$info[] = $this->t('<strong>Back up your database</strong>. This process will change your database values and in case of emergency you may need to revert to a backup.');
|
||||
$info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
|
||||
|
@ -388,7 +388,7 @@ class DbUpdateController extends ControllerBase {
|
|||
$dblog_exists = $this->moduleHandler->moduleExists('dblog');
|
||||
if ($dblog_exists && $this->account->hasPermission('access site reports')) {
|
||||
$log_message = $this->t('All errors have been <a href="@url">logged</a>.', array(
|
||||
'@url' => $this->url('dblog.overview'),
|
||||
'@url' => Url::fromRoute('dblog.overview')->toString(TRUE)->getGeneratedUrl(),
|
||||
));
|
||||
}
|
||||
else {
|
||||
|
@ -396,7 +396,7 @@ class DbUpdateController extends ControllerBase {
|
|||
}
|
||||
|
||||
if (!empty($_SESSION['update_success'])) {
|
||||
$message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => $this->url('<front>'))) . ' ' . $log_message . '</p>';
|
||||
$message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('<front>')->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '</p>';
|
||||
}
|
||||
else {
|
||||
$last = reset($_SESSION['updates_remaining']);
|
||||
|
@ -497,7 +497,7 @@ class DbUpdateController extends ControllerBase {
|
|||
*/
|
||||
public function requirements($severity, array $requirements) {
|
||||
$options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array();
|
||||
$try_again_url = $this->url('system.db_update', $options);
|
||||
$try_again_url = Url::fromRoute('system.db_update', $options)->toString(TRUE)->getGeneratedUrl();
|
||||
|
||||
$build['status_report'] = array(
|
||||
'#theme' => 'status_report',
|
||||
|
|
|
@ -42,6 +42,16 @@ class EarlyRenderingControllerTest extends WebTestBase {
|
|||
$this->assertRaw('Hello world!');
|
||||
$this->assertCacheTag('foo');
|
||||
|
||||
// AjaxResponse: non-early & early.
|
||||
// @todo Add cache tags assertion when AjaxResponse is made cacheable in
|
||||
// https://www.drupal.org/node/956186.
|
||||
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertRaw('Hello world!');
|
||||
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.ajax_response.early'));
|
||||
$this->assertResponse(200);
|
||||
$this->assertRaw('Hello world!');
|
||||
|
||||
// Basic Response object: non-early & early.
|
||||
$this->drupalGet(Url::fromRoute('early_rendering_controller_test.response'));
|
||||
$this->assertResponse(200);
|
||||
|
|
|
@ -65,7 +65,7 @@ class PagerTest extends WebTestBase {
|
|||
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--last'));
|
||||
preg_match('@page=(\d+)@', $elements[0]['href'], $matches);
|
||||
$current_page = (int) $matches[1];
|
||||
$this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
|
||||
$this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]['href'], array('external' => TRUE));
|
||||
$this->assertPagerItems($current_page);
|
||||
}
|
||||
|
||||
|
@ -77,18 +77,22 @@ class PagerTest extends WebTestBase {
|
|||
$this->drupalGet('pager-test/query-parameters');
|
||||
$this->assertText(t('Pager calls: 0'), 'Initial call to pager shows 0 calls.');
|
||||
$this->assertText('pager.0.0');
|
||||
$this->assertCacheContext('url.query_args');
|
||||
|
||||
// Go to last page, the count of pager calls need to go to 1.
|
||||
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--last'));
|
||||
$this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
|
||||
$this->drupalGet($this->getAbsoluteUrl($elements[0]['href']));
|
||||
$this->assertText(t('Pager calls: 1'), 'First link call to pager shows 1 calls.');
|
||||
$this->assertText('pager.0.60');
|
||||
$this->assertCacheContext('url.query_args');
|
||||
|
||||
// Go back to first page, the count of pager calls need to go to 2.
|
||||
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--first'));
|
||||
$this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE));
|
||||
$this->drupalGet($this->getAbsoluteUrl($elements[0]['href']));
|
||||
$this->drupalGet($GLOBALS['base_root'] . parse_url($this->getUrl())['path'] . $elements[0]['href'], array('external' => TRUE));
|
||||
$this->assertText(t('Pager calls: 2'), 'Second link call to pager shows 2 calls.');
|
||||
$this->assertText('pager.0.0');
|
||||
$this->assertCacheContext('url.query_args');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\system\Tests\Render\UrlBubbleableMetadataBubblingTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\system\Tests\Render;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests that URL bubbleable metadata is correctly bubbled.
|
||||
*
|
||||
* @group Render
|
||||
*/
|
||||
class UrlBubbleableMetadataBubblingTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['cache_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->dumpHeaders = TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that URL bubbleable metadata is correctly bubbled.
|
||||
*/
|
||||
public function testUrlBubbleableMetadataBubbling() {
|
||||
// Test that regular URLs bubble up bubbleable metadata when converted to
|
||||
// string.
|
||||
$url = Url::fromRoute('cache_test.url_bubbling');
|
||||
$this->drupalGet($url);
|
||||
$this->assertCacheContext('url.site');
|
||||
$this->assertRaw($url->setAbsolute()->toString());
|
||||
}
|
||||
|
||||
}
|
|
@ -657,7 +657,7 @@ function system_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
|
|||
|
||||
$pathPrefix = '';
|
||||
$current_query = $request->query->all();
|
||||
Url::fromRoute('<front>', [], array('script' => &$scriptPath, 'prefix' => &$pathPrefix))->toString();
|
||||
Url::fromRoute('<front>', [], array('script' => &$scriptPath, 'prefix' => &$pathPrefix))->toString(TRUE);
|
||||
$current_path = \Drupal::routeMatch()->getRouteName() ? Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath() : '';
|
||||
$current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
|
||||
$path_settings = [
|
||||
|
|
|
@ -385,7 +385,7 @@ system.theme_settings_theme:
|
|||
'<none>':
|
||||
path: ''
|
||||
options:
|
||||
_only_fragment: TRUE
|
||||
_no_path: TRUE
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
cache_test.url_bubbling:
|
||||
path: '/cache-test/url-bubbling'
|
||||
defaults:
|
||||
_controller: '\Drupal\cache_test\Controller\CacheTestController::urlBubbling'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\cache_test\Controller\CacheTestController.
|
||||
*/
|
||||
|
||||
namespace Drupal\cache_test\Controller;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Render\HtmlResponse;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Controller routines for cache_test routes.
|
||||
*/
|
||||
class CacheTestController {
|
||||
|
||||
/**
|
||||
* Early renders a URL to test bubbleable metadata bubbling.
|
||||
*/
|
||||
public function urlBubbling() {
|
||||
$url = Url::fromRoute('<current>')->setAbsolute();
|
||||
return [
|
||||
'#markup' => SafeMarkup::format('This URL is early-rendered: !url. Yet, its bubbleable metadata should be bubbled.', ['!url' => $url->toString()])
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,20 @@ early_rendering_controller_test.render_array.early:
|
|||
requirements:
|
||||
_access: 'TRUE'
|
||||
|
||||
# Controller returning an AjaxResponse.
|
||||
early_rendering_controller_test.ajax_response:
|
||||
path: '/early-rendering-controller-test/ajax-response'
|
||||
defaults:
|
||||
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::ajaxResponse'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
early_rendering_controller_test.ajax_response.early:
|
||||
path: '/early-rendering-controller-test/ajax-response/early'
|
||||
defaults:
|
||||
_controller: '\Drupal\early_rendering_controller_test\EarlyRenderingTestController::ajaxResponseEarly'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
|
||||
# Controller returning a basic Response object.
|
||||
early_rendering_controller_test.response:
|
||||
path: '/early-rendering-controller-test/response'
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
namespace Drupal\early_rendering_controller_test;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\InsertCommand;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
@ -76,6 +78,18 @@ class EarlyRenderingTestController extends ControllerBase {
|
|||
];
|
||||
}
|
||||
|
||||
public function ajaxResponse() {
|
||||
$response = new AjaxResponse();
|
||||
$response->addCommand(new InsertCommand(NULL, $this->renderArray()));
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function ajaxResponseEarly() {
|
||||
$response = new AjaxResponse();
|
||||
$response->addCommand(new InsertCommand(NULL, $this->renderArrayEarly()));
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function response() {
|
||||
return new Response('Hello world!');
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
|||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
|
||||
use Drupal\Core\Path\CurrentPathStack;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Routing\RedirectDestinationInterface;
|
||||
use Drupal\views\Ajax\ScrollTopCommand;
|
||||
|
@ -174,9 +176,18 @@ class ViewAjaxController implements ContainerInjectionInterface {
|
|||
// Reuse the same DOM id so it matches that in drupalSettings.
|
||||
$view->dom_id = $dom_id;
|
||||
|
||||
if ($preview = $view->preview($display_id, $args)) {
|
||||
$response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
|
||||
$context = new RenderContext();
|
||||
$preview = $this->renderer->executeInRenderContext($context, function() use ($view, $display_id, $args) {
|
||||
return $view->preview($display_id, $args);
|
||||
});
|
||||
if (!$context->isEmpty()) {
|
||||
$bubbleable_metadata = $context->pop();
|
||||
BubbleableMetadata::createFromRenderArray($preview)
|
||||
->merge($bubbleable_metadata)
|
||||
->applyTo($preview);
|
||||
}
|
||||
$response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
|
||||
|
||||
return $response;
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -96,6 +96,7 @@ class Full extends SqlBase {
|
|||
'#element' => $this->options['id'],
|
||||
'#parameters' => $input,
|
||||
'#quantity' => $this->options['quantity'],
|
||||
'#route_name' => !empty($this->view->live_preview) ? '<current>' : '<none>',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ class Mini extends SqlBase {
|
|||
'#tags' => $tags,
|
||||
'#element' => $this->options['id'],
|
||||
'#parameters' => $input,
|
||||
'#route_name' => !empty($this->view->live_preview) ? '<current>' : '<none>',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -382,14 +382,9 @@ abstract class SqlBase extends PagerPluginBase implements CacheablePluginInterfa
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
$contexts = ['url.query_args.pagers:' . $this->options['id']];
|
||||
if ($this->options['expose']['items_per_page']) {
|
||||
$contexts[] = 'url.query_args:items_per_page';
|
||||
}
|
||||
if ($this->options['expose']['offset']) {
|
||||
$contexts[] = 'url.query_args:offset';
|
||||
}
|
||||
return $contexts;
|
||||
// The rendered link needs to play well with any other query parameter used
|
||||
// on the page, like other pagers and exposed filter.
|
||||
return ['url.query_args'];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -444,8 +444,9 @@ class Table extends StylePluginBase implements CacheablePluginInterface {
|
|||
|
||||
foreach ($this->options['info'] as $field_id => $info) {
|
||||
if (!empty($info['sortable'])) {
|
||||
$contexts[] = 'url.query_args:order';
|
||||
$contexts[] = 'url.query_args:sort';
|
||||
// The rendered link needs to play well with any other query parameter
|
||||
// used on the page, like pager and exposed filter.
|
||||
$contexts[] = 'url.query_args';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,17 +71,29 @@ class GlossaryTest extends ViewTestBase {
|
|||
$url = Url::fromRoute('view.glossary.page_1');
|
||||
|
||||
// Verify cache tags.
|
||||
$this->assertPageCacheContextsAndTags($url, ['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url', 'user.node_grants:view', 'user.permissions'], [
|
||||
'config:views.view.glossary',
|
||||
'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
|
||||
'node_list',
|
||||
'user:0',
|
||||
'user_list',
|
||||
'rendered',
|
||||
// FinishResponseSubscriber adds this cache tag to responses that have the
|
||||
// 'user.permissions' cache context for anonymous users.
|
||||
'config:user.role.anonymous',
|
||||
]);
|
||||
$this->assertPageCacheContextsAndTags(
|
||||
$url,
|
||||
[
|
||||
'languages:' . LanguageInterface::TYPE_CONTENT,
|
||||
'languages:' . LanguageInterface::TYPE_INTERFACE,
|
||||
'theme',
|
||||
'url',
|
||||
'user.node_grants:view',
|
||||
'user.permissions',
|
||||
'route',
|
||||
],
|
||||
[
|
||||
'config:views.view.glossary',
|
||||
'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
|
||||
'node_list',
|
||||
'user:0',
|
||||
'user_list',
|
||||
'rendered',
|
||||
// FinishResponseSubscriber adds this cache tag to responses that have the
|
||||
// 'user.permissions' cache context for anonymous users.
|
||||
'config:user.role.anonymous',
|
||||
]
|
||||
);
|
||||
|
||||
// Check the actual page response.
|
||||
$this->drupalGet($url);
|
||||
|
|
|
@ -68,23 +68,21 @@ class FieldWebTest extends HandlerTestBase {
|
|||
$this->assertResponse(200);
|
||||
|
||||
// Only the id and name should be click sortable, but not the name.
|
||||
$this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'id', 'sort' => 'asc']]));
|
||||
$this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'name', 'sort' => 'desc']]));
|
||||
$this->assertNoLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'created']]));
|
||||
$this->assertLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'id', 'sort' => 'asc']]));
|
||||
$this->assertLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'name', 'sort' => 'desc']]));
|
||||
$this->assertNoLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'created']]));
|
||||
|
||||
// Check that the view returns the click sorting cache contexts.
|
||||
$expected_contexts = [
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'url.query_args.pagers:0',
|
||||
'url.query_args:order',
|
||||
'url.query_args:sort',
|
||||
'url.query_args',
|
||||
];
|
||||
$this->assertCacheContexts($expected_contexts);
|
||||
|
||||
// Clicking a click sort should change the order.
|
||||
$this->clickLink(t('ID'));
|
||||
$this->assertLinkByHref(\Drupal::url('view.test_click_sort.page_1', [], ['query' => ['order' => 'id', 'sort' => 'desc']]));
|
||||
$this->assertLinkByHref(\Drupal::url('<none>', [], ['query' => ['order' => 'id', 'sort' => 'desc']]));
|
||||
// Check that the output has the expected order (asc).
|
||||
$ids = $this->clickSortLoadIdsFromOutput();
|
||||
$this->assertEqual($ids, range(1, 5));
|
||||
|
|
|
@ -206,11 +206,7 @@ class ExposedFormTest extends ViewTestBase {
|
|||
'languages:language_interface',
|
||||
'entity_test_view_grants',
|
||||
'theme',
|
||||
'url.query_args.pagers:0',
|
||||
'url.query_args:items_per_page',
|
||||
'url.query_args:offset',
|
||||
'url.query_args:sort_order',
|
||||
'url.query_args:sort_by',
|
||||
'url.query_args',
|
||||
'languages:language_content'
|
||||
];
|
||||
|
||||
|
|
|
@ -261,7 +261,7 @@ class PagerTest extends PluginTestBase {
|
|||
|
||||
// Test pager cache contexts.
|
||||
$this->drupalGet('test_pager_full');
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'timezone', 'url.query_args.pagers:0', 'user.node_grants:view']);
|
||||
$this->assertCacheContexts(['languages:language_interface', 'theme', 'timezone', 'url.query_args', 'user.node_grants:view']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -292,7 +292,7 @@ class RenderCacheIntegrationTest extends ViewUnitTestBase {
|
|||
$view = View::load('test_display');
|
||||
$view->save();
|
||||
|
||||
$this->assertEqual(['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions'], $view->getDisplay('default')['cache_metadata']['contexts']);
|
||||
$this->assertEqual(['languages:' . LanguageInterface::TYPE_CONTENT, 'languages:' . LanguageInterface::TYPE_INTERFACE, 'url.query_args', 'user.node_grants:view', 'user.permissions'], $view->getDisplay('default')['cache_metadata']['contexts']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
namespace Drupal\Tests\views\Unit\Controller {
|
||||
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\views\Ajax\ViewAjaxResponse;
|
||||
use Drupal\views\Controller\ViewAjaxController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\views\Controller\ViewAjaxController
|
||||
|
@ -76,6 +78,11 @@ class ViewAjaxControllerTest extends UnitTestCase {
|
|||
$elements['#attached'] = [];
|
||||
return isset($elements['#markup']) ? $elements['#markup'] : '';
|
||||
}));
|
||||
$this->renderer->expects($this->any())
|
||||
->method('executeInRenderContext')
|
||||
->willReturnCallback(function (RenderContext $context, callable $callable) {
|
||||
return $callable();
|
||||
});
|
||||
$this->currentPath = $this->getMockBuilder('Drupal\Core\Path\CurrentPathStack')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
@ -83,8 +90,23 @@ class ViewAjaxControllerTest extends UnitTestCase {
|
|||
|
||||
$this->viewAjaxController = new ViewAjaxController($this->viewStorage, $this->executableFactory, $this->renderer, $this->currentPath, $this->redirectDestination);
|
||||
|
||||
$request_stack = new RequestStack();
|
||||
$request_stack->push(new Request());
|
||||
$args = [
|
||||
$this->getMock('\Drupal\Core\Controller\ControllerResolverInterface'),
|
||||
$this->getMock('\Drupal\Core\Theme\ThemeManagerInterface'),
|
||||
$this->getMock('\Drupal\Core\Render\ElementInfoManagerInterface'),
|
||||
$this->getMock('\Drupal\Core\Render\RenderCacheInterface'),
|
||||
$request_stack,
|
||||
[
|
||||
'required_cache_contexts' => [
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->renderer = $this->getMockBuilder('Drupal\Core\Render\Renderer')
|
||||
->disableOriginalConstructor()
|
||||
->setConstructorArgs($args)
|
||||
->setMethods(NULL)
|
||||
->getMock();
|
||||
$container = new ContainerBuilder();
|
||||
|
|
|
@ -494,7 +494,9 @@ function template_preprocess_views_view_table(&$variables) {
|
|||
'attributes' => array('title' => $title),
|
||||
'query' => $query,
|
||||
);
|
||||
$variables['header'][$field]['content'] = \Drupal::l($label, new Url('<current>', [], $link_options));
|
||||
// It is ok to specify no URL path here as we will always reload the
|
||||
// current page.
|
||||
$variables['header'][$field]['content'] = \Drupal::l($label, new Url('<none>', [], $link_options));
|
||||
}
|
||||
|
||||
$variables['header'][$field]['default_classes'] = $fields[$field]->options['element_default_classes'];
|
||||
|
@ -1050,6 +1052,10 @@ function template_preprocess_views_mini_pager(&$variables) {
|
|||
}
|
||||
$variables['items']['next']['attributes'] = new Attribute();
|
||||
}
|
||||
|
||||
// This is is based on the entire current query string. We need to ensure
|
||||
// cacheability is affected accordingly.
|
||||
$variables['#cache']['contexts'][] = 'url.query_args';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Drupal\Tests\Core\Render\Element;
|
|||
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\GeneratedUrl;
|
||||
use Drupal\Core\Render\Element\RenderElement;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
@ -56,8 +57,8 @@ class RenderElementTest extends UnitTestCase {
|
|||
|
||||
$prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
|
||||
$url = '/test?foo=bar&ajax_form=1';
|
||||
$prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
|
||||
->willReturn($url);
|
||||
$prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], TRUE)
|
||||
->willReturn((new GeneratedUrl())->setCacheContexts(['route'])->setGeneratedUrl($url));
|
||||
|
||||
$url_generator = $prophecy->reveal();
|
||||
$this->container->set('url_generator', $url_generator);
|
||||
|
@ -87,8 +88,8 @@ class RenderElementTest extends UnitTestCase {
|
|||
|
||||
$prophecy = $this->prophesize('Drupal\Core\Routing\UrlGeneratorInterface');
|
||||
$url = '/test?foo=bar&other=query&ajax_form=1';
|
||||
$prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], FALSE)
|
||||
->willReturn($url);
|
||||
$prophecy->generateFromRoute('<current>', [], ['query' => ['foo' => 'bar', 'other' => 'query', FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], TRUE)
|
||||
->willReturn((new GeneratedUrl())->setCacheContexts(['route'])->setGeneratedUrl($url));
|
||||
|
||||
$url_generator = $prophecy->reveal();
|
||||
$this->container->set('url_generator', $url_generator);
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\Core\Render\MetadataBubblingUrlGeneratorTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\Core\Render;
|
||||
|
||||
use Drupal\Core\Render\MetadataBubblingUrlGenerator;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\Core\Routing\UrlGeneratorTest;
|
||||
|
||||
/**
|
||||
* Confirm that the MetadataBubblingUrlGenerator is functioning properly.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\Core\Render\MetadataBubblingUrlGenerator
|
||||
*
|
||||
* @group Render
|
||||
*/
|
||||
class MetadataBubblingUrlGeneratorTest extends UrlGeneratorTest {
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
|
||||
$this->renderer->expects($this->any())
|
||||
->method('hasRenderContext')
|
||||
->willReturn(TRUE);
|
||||
|
||||
$this->generator = new MetadataBubblingUrlGenerator($this->generator, $this->renderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests bubbling of cacheable metadata for URLs.
|
||||
*
|
||||
* @param bool $collect_bubbleable_metadata
|
||||
* Whether bubbleable metadata should be collected.
|
||||
* @param int $invocations
|
||||
* The expected amount of invocations for the ::bubble() method.
|
||||
* @param array $options
|
||||
* The URL options.
|
||||
*
|
||||
* @covers ::bubble
|
||||
*
|
||||
* @dataProvider providerUrlBubbleableMetadataBubbling
|
||||
*/
|
||||
public function testUrlBubbleableMetadataBubbling($collect_bubbleable_metadata, $invocations, array $options) {
|
||||
$self = $this;
|
||||
|
||||
$this->renderer->expects($this->exactly($invocations))
|
||||
->method('render')
|
||||
->willReturnCallback(function ($build) use ($self) {
|
||||
$self->assertTrue(!empty($build['#cache']));
|
||||
});
|
||||
|
||||
$url = new Url('test_1', [], $options);
|
||||
$url->setUrlGenerator($this->generator);
|
||||
$url->toString($collect_bubbleable_metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testUrlBubbleableMetadataBubbling().
|
||||
*/
|
||||
public function providerUrlBubbleableMetadataBubbling() {
|
||||
return [
|
||||
// No bubbling when bubbleable metadata is collected.
|
||||
[TRUE, 0, []],
|
||||
// Bubbling when bubbleable metadata is not collected.
|
||||
[FALSE, 1, []],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -11,10 +11,9 @@ use Drupal\Core\Cache\Cache;
|
|||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\Context\ContextCacheKeys;
|
||||
use Drupal\Core\Cache\MemoryBackend;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Render\Renderer;
|
||||
use Drupal\Core\Render\RenderCache;
|
||||
use Drupal\Core\Render\Renderer;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
@ -102,6 +101,9 @@ class RendererTestBase extends UnitTestCase {
|
|||
$this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
|
||||
$this->elementInfo = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface');
|
||||
$this->requestStack = new RequestStack();
|
||||
$request = new Request();
|
||||
$request->server->set('REQUEST_TIME', $_SERVER['REQUEST_TIME']);
|
||||
$this->requestStack->push($request);
|
||||
$this->cacheFactory = $this->getMock('Drupal\Core\Cache\CacheFactoryInterface');
|
||||
$this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
|
||||
->disableOriginalConstructor()
|
||||
|
@ -129,7 +131,7 @@ class RendererTestBase extends UnitTestCase {
|
|||
return new ContextCacheKeys($keys, new CacheableMetadata());
|
||||
});
|
||||
$this->renderCache = new RenderCache($this->requestStack, $this->cacheFactory, $this->cacheContextsManager);
|
||||
$this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->rendererConfig);
|
||||
$this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->requestStack, $this->rendererConfig);
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->set('cache_contexts_manager', $this->cacheContextsManager);
|
||||
|
|
|
@ -23,6 +23,7 @@ use Symfony\Component\Routing\RouteCollection;
|
|||
/**
|
||||
* Confirm that the UrlGenerator is functioning properly.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\Core\Routing\UrlGenerator
|
||||
* @group Routing
|
||||
*/
|
||||
class UrlGeneratorTest extends UnitTestCase {
|
||||
|
@ -70,11 +71,14 @@ class UrlGeneratorTest extends UnitTestCase {
|
|||
$first_route = new Route('/test/one');
|
||||
$second_route = new Route('/test/two/{narf}');
|
||||
$third_route = new Route('/test/two/');
|
||||
$fourth_route = new Route('/test/four', array(), array(), array(), '', ['https']);
|
||||
$fourth_route = new Route('/test/four', [], [], [], '', ['https']);
|
||||
$none_route = new Route('', [], [], ['_no_path' => TRUE]);
|
||||
|
||||
$routes->add('test_1', $first_route);
|
||||
$routes->add('test_2', $second_route);
|
||||
$routes->add('test_3', $third_route);
|
||||
$routes->add('test_4', $fourth_route);
|
||||
$routes->add('<none>', $none_route);
|
||||
|
||||
// Create a route provider stub.
|
||||
$provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider')
|
||||
|
@ -85,22 +89,26 @@ class UrlGeneratorTest extends UnitTestCase {
|
|||
// are not passed in and default to an empty array.
|
||||
$route_name_return_map = $routes_names_return_map = array();
|
||||
$return_map_values = array(
|
||||
array(
|
||||
[
|
||||
'route_name' => 'test_1',
|
||||
'return' => $first_route,
|
||||
),
|
||||
array(
|
||||
],
|
||||
[
|
||||
'route_name' => 'test_2',
|
||||
'return' => $second_route,
|
||||
),
|
||||
array(
|
||||
],
|
||||
[
|
||||
'route_name' => 'test_3',
|
||||
'return' => $third_route,
|
||||
),
|
||||
array(
|
||||
],
|
||||
[
|
||||
'route_name' => 'test_4',
|
||||
'return' => $fourth_route,
|
||||
),
|
||||
],
|
||||
[
|
||||
'route_name' => '<none>',
|
||||
'return' => $none_route,
|
||||
],
|
||||
);
|
||||
foreach ($return_map_values as $values) {
|
||||
$route_name_return_map[] = array($values['route_name'], $values['return']);
|
||||
|
@ -414,6 +422,43 @@ class UrlGeneratorTest extends UnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating a relative URL with no path.
|
||||
*
|
||||
* @param array $options
|
||||
* An array of URL options.
|
||||
* @param string $expected_url
|
||||
* The expected relative URL.
|
||||
*
|
||||
* @covers ::generateFromRoute
|
||||
*
|
||||
* @dataProvider providerTestNoPath
|
||||
*/
|
||||
public function testNoPath($options, $expected_url) {
|
||||
$url = $this->generator->generateFromRoute('<none>', [], $options);
|
||||
$this->assertEquals($expected_url, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testNoPath().
|
||||
*/
|
||||
public function providerTestNoPath() {
|
||||
return [
|
||||
// Empty options.
|
||||
[[], ''],
|
||||
// Query parameters only.
|
||||
[['query' => ['foo' => 'bar']], '?foo=bar'],
|
||||
// Multiple query parameters.
|
||||
[['query' => ['foo' => 'bar', 'baz' => '']], '?foo=bar&baz='],
|
||||
// Fragment only.
|
||||
[['fragment' => 'foo'], '#foo'],
|
||||
// Query parameters and fragment.
|
||||
[['query' => ['bar' => 'baz'], 'fragment' => 'foo'], '?bar=baz#foo'],
|
||||
// Multiple query parameters and fragment.
|
||||
[['query' => ['bar' => 'baz', 'foo' => 'bar'], 'fragment' => 'foo'], '?bar=baz&foo=bar#foo'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue