Update to drupal 8.0.0-rc1. For more information, see https://www.drupal.org/node/2582663

This commit is contained in:
Greg Anderson 2015-10-08 11:40:12 -07:00
parent eb34d130a8
commit f32e58e4b1
8476 changed files with 211648 additions and 170042 deletions

View file

@ -9,6 +9,8 @@ namespace Drupal\Core\Render;
/**
* Defines an interface for processing attachments of responses that have them.
*
* @see \Drupal\Core\Ajax\AjaxResponse
* @see \Drupal\Core\Ajax\AjaxResponseAttachmentsProcessor
* @see \Drupal\Core\Render\HtmlResponse
* @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
*/
@ -17,13 +19,38 @@ interface AttachmentsResponseProcessorInterface {
/**
* Processes the attachments of a response that has attachments.
*
* Libraries, JavaScript settings, feeds, HTML <head> tags, HTML <head> links,
* HTTP headers, and the HTTP status code are attached to render arrays using
* the #attached property. The #attached property is an associative array,
* where the keys are the attachment types and the values are the attached
* data. For example:
*
* @code
* $build['#attached']['library'][] = [
* 'library' => ['core/jquery']
* ];
* $build['#attached']['http_header'][] = [
* ['Content-Type', 'application/rss+xml; charset=utf-8'],
* ];
* @endcode
*
* The available keys are:
* - 'library' (asset libraries)
* - 'drupalSettings' (JavaScript settings)
* - 'feed' (RSS feeds)
* - 'html_head' (tags in HTML <head>)
* - 'html_head_link' (<link> tags in HTML <head>)
* - 'http_header' (HTTP headers and status code)
*
* @param \Drupal\Core\Render\AttachmentsInterface $response
* The response to process the attachments for.
* The response to process.
*
* @return \Drupal\Core\Render\AttachmentsInterface
* The processed response.
*
* @throws \InvalidArgumentException
* Thrown when the $response parameter is not the type of response object
* the processor expects.
*/
public function processAttachments(AttachmentsInterface $response);

View file

@ -13,8 +13,18 @@ use Drupal\Core\Render\Element;
/**
* Provides a wrapper element to group one or more buttons in a form.
*
* Use of the 'actions' element as an array key helps to ensure proper styling
* in themes and to enable other modules to properly alter a form's actions.
* Use of a single Actions element with an array key of 'actions' to group the
* primary submit buttons on a form helps to ensure proper styling in themes,
* and enables other modules to properly alter a form's actions.
*
* Usage example:
* @code
* $form['actions'] = array('#type' => 'actions');
* $form['actions']['submit'] = array(
* '#type' => 'submit',
* '#value' => t('Save'),
* );
* @endcode
*
* @RenderElement("actions")
*/

View file

@ -16,6 +16,31 @@ use Drupal\Core\Form\FormStateInterface;
* Surrounds child elements with a <div> and adds attributes such as classes or
* an HTML ID.
*
* Usage example:
* @code
* $form['needs_accommodation'] = array(
* '#type' => 'checkbox',
* '#title' => 'Need Special Accommodations?',
* );
*
* $form['accommodation'] = array(
* '#type' => 'container',
* '#attributes' => array(
* 'class' => 'accommodation',
* ),
* '#states' => array(
* 'invisible' => array(
* 'input[name="needs_accommodation"]' => array('checked' => FALSE),
* ),
* ),
* );
*
* $form['accommodation']['diet'] = array(
* '#type' => 'textfield',
* '#title' => t('Dietary Restrictions'),
* );
* @endcode
*
* @RenderElement("container")
*/
class Container extends RenderElement {

View file

@ -13,9 +13,29 @@ use Drupal\Core\Render\Element;
* Provides a render element for a details element, similar to a fieldset.
*
* Fieldsets can only be used in forms, while details elements can be used
* outside of forms.
* outside of forms. Users click on the the title to open or close the details
* element, showing or hiding the contained elements.
*
* Properties:
* - #title: The title of the details container. Defaults to "Details".
* - #open: Indicates whether the container should be open by default.
* Defaults to FALSE.
*
* Usage example:
* @code
* $form['author'] = array(
* '#type' => 'details',
* '#title' => 'Author',
* );
*
* $form['author']['name'] = array(
* '#type' => 'textfield',
* '#title' => t('Name'),
* );
* @endcode
*
* @see \Drupal\Core\Render\Element\Fieldset
* @see \Drupal]Core\Render\Element\VerticalTabs
*
* @RenderElement("details")
*/

View file

@ -11,7 +11,10 @@ namespace Drupal\Core\Render\Element;
* Provides a render element for a group of form elements.
*
* In default rendering, the only difference between a 'fieldgroup' and a
* 'fieldset' is the CSS class applied to the containing HTML element.
* 'fieldset' is the CSS class applied to the containing HTML element. Normally
* use a fieldset.
*
* @see \Drupal\Core\Render\Element\Fieldset for documentation and usage.
*
* @see \Drupal\Core\Render\Element\Fieldset
* @see \Drupal\Core\Render\Element\Details

View file

@ -12,8 +12,18 @@ use Drupal\Core\Render\Element;
/**
* Provides a render element for a group of form elements.
*
* In default rendering, the only difference between a 'fieldgroup' and a
* 'fieldset' is the CSS class applied to the containing HTML element.
* Usage example:
* @code
* $form['author'] = array(
* '#type' => 'fieldset',
* '#title' => 'Author',
* );
*
* $form['author']['name'] = array(
* '#type' => 'textfield',
* '#title' => t('Name'),
* );
* @endcode
*
* @see \Drupal\Core\Render\Element\Fieldgroup
* @see \Drupal\Core\Render\Element\Details

View file

@ -9,7 +9,7 @@ namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html as HtmlUtility;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\SafeString;
use Drupal\Core\Render\Markup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Template\Attribute;
@ -85,7 +85,7 @@ class HtmlTag extends RenderElement {
if (!empty($element['#noscript'])) {
$markup = "<noscript>$markup</noscript>";
}
$element['#markup'] = SafeString::create($markup);
$element['#markup'] = Markup::create($markup);
return $element;
}
@ -166,13 +166,13 @@ class HtmlTag extends RenderElement {
// filtered if they are unsafe. Thus, all these strings are safe.
if (!$browsers['!IE']) {
// "downlevel-hidden".
$element['#prefix'] = SafeString::create("\n<!--[if $expression]>\n" . $prefix);
$element['#suffix'] = SafeString::create($suffix . "<![endif]-->\n");
$element['#prefix'] = Markup::create("\n<!--[if $expression]>\n" . $prefix);
$element['#suffix'] = Markup::create($suffix . "<![endif]-->\n");
}
else {
// "downlevel-revealed".
$element['#prefix'] = SafeString::create("\n<!--[if $expression]><!-->\n" . $prefix);
$element['#suffix'] = SafeString::create($suffix . "<!--<![endif]-->\n");
$element['#prefix'] = Markup::create("\n<!--[if $expression]><!-->\n" . $prefix);
$element['#suffix'] = Markup::create($suffix . "<!--<![endif]-->\n");
}
return $element;

View file

@ -38,17 +38,18 @@ class Link extends RenderElement {
* Doing so during pre_render gives modules a chance to alter the link parts.
*
* @param array $element
* A structured array whose keys form the arguments to _l():
* - #title: The link text to pass as argument to _l().
* A structured array whose keys form the arguments to
* \Drupal\Core\Utility\LinkGeneratorInterface::generate():
* - #title: The link text.
* - #url: The URL info either pointing to a route or a non routed path.
* - #options: (optional) An array of options to pass to _l() or the link
* generator.
* - #options: (optional) An array of options to pass to the link generator.
*
* @return array
* The passed-in element containing a rendered link in '#markup'.
*/
public static function preRenderLink($element) {
// By default, link options to pass to _l() are normally set in #options.
// By default, link options to pass to the link generator are normally set
// in #options.
$element += array('#options' => array());
// However, within the scope of renderable elements, #attributes is a valid
// way to specify attributes, too. Take them into account, but do not override
@ -82,7 +83,7 @@ class Link extends RenderElement {
$options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']);
/** @var \Drupal\Core\Utility\LinkGenerator $link_generator */
$link_generator = \Drupal::service('link_generator');
$generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
$generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options));
$element['#markup'] = $generated_link->getGeneratedLink();
$generated_link->merge(BubbleableMetadata::createFromRenderArray($element))
->applyTo($element);

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\PageTitle.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for the title of an HTML page.
*
* This represents the title of the HTML page's body.
*
* @RenderElement("page_title")
*/
class PageTitle extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#theme' => 'page_title',
// The page title: either a string for plain titles or a render array for
// formatted titles.
'#title' => NULL,
];
}
}

View file

@ -102,7 +102,7 @@ class PasswordConfirm extends FormElement {
public static function validatePasswordConfirm(&$element, FormStateInterface $form_state, &$complete_form) {
$pass1 = trim($element['pass1']['#value']);
$pass2 = trim($element['pass2']['#value']);
if (!empty($pass1) || !empty($pass2)) {
if (strlen($pass1) > 0 || strlen($pass2) > 0) {
if (strcmp($pass1, $pass2)) {
$form_state->setError($element, t('The specified passwords do not match.'));
}

View file

@ -17,6 +17,41 @@ use Drupal\Component\Utility\Html as HtmlUtility;
* Note: Although this extends FormElement, it can be used outside the
* context of a form.
*
* Properties:
* - #header: An array of table header labels.
* - #rows: An array of the rows to be displayed. Each row is either an array
* of cell contents or an array of properties as described in table.html.twig
* Alternatively specify the data for the table as child elements of the table
* element. Table elements would contain rows elements that would in turn
* contain column elements.
* - #empty: Text to display when no rows are present.
* - #responsive: Indicates whether to add the drupal.responsive_table library
* providing responsive tables. Defaults to TRUE.
* - #sticky: Indicates whether to add the drupal.tableheader library that makes
* table headers always visible at the top of the page. Defaults to FALSE.
*
* Usage example:
* @code
* $form['contacts'] = array(
* '#type' => 'table',
* '#title' => 'Sample Table',
* '#header' => array('Name', 'Phone'),
* );
*
* for ($i=1; $i<=4; $i++) {
* $form['contacts'][$i]['name'] = array(
* '#type' => 'textfield',
* '#title' => t('Name'),
* '#title_display' => 'invisible',
* );
*
* $form['contacts'][$i]['phone'] = array(
* '#type' => 'tel',
* '#title' => t('Phone'),
* '#title_display' => 'invisible',
* );
* }
* @endcode
* @see \Drupal\Core\Render\Element\Tableselect
*
* @FormElement("table")
@ -165,7 +200,7 @@ class Table extends FormElement {
}
}
if (isset($title) && $title !== '') {
$title = t('Update !title', array('!title' => $title));
$title = t('Update @title', array('@title' => $title));
}
}
@ -297,7 +332,7 @@ class Table extends FormElement {
* @return array
*
* @see template_preprocess_table()
* @see drupal_process_attached()
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
* @see drupal_attach_tabledrag()
*/
public static function preRenderTable($element) {

View file

@ -10,6 +10,7 @@ namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html as HtmlUtility;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a form element for a table with radios or checkboxes in left column.
@ -217,10 +218,12 @@ class Tableselect extends Table {
if (!isset($element[$key])) {
if ($element['#multiple']) {
$title = '';
if (!empty($element['#options'][$key]['title']['data']['#title'])) {
$title = t('Update @title', array(
'@title' => $element['#options'][$key]['title']['data']['#title'],
));
if (isset($element['#options'][$key]['title']) && is_array($element['#options'][$key]['title'])) {
if (!empty($element['#options'][$key]['title']['data']['#title'])) {
$title = new TranslatableMarkup('Update @title', array(
'@title' => $element['#options'][$key]['title']['data']['#title'],
));
}
}
$element[$key] = array(
'#type' => 'checkbox',

View file

@ -13,8 +13,42 @@ use Drupal\Core\Render\Element;
/**
* Provides a render element for vertical tabs in a form.
*
* Formats all child fieldsets and all non-child fieldsets whose #group is
* assigned this element's name as vertical tabs.
* Formats all child and non-child details elements whose #group is assigned
* this element's name as vertical tabs.
*
* Properties:
* - #default_tab: The HTML ID of the rendered details element to be used as
* the default tab. View the source of the rendered page to determine the ID.
*
* Usage example:
* @code
* $form['information'] = array(
* '#type' => 'vertical_tabs',
* '#default_tab' => 'edit-publication',
* );
*
* $form['author'] = array(
* '#type' => 'details',
* '#title' => 'Author',
* '#group' => 'information',
* );
*
* $form['author']['name'] = array(
* '#type' => 'textfield',
* '#title' => t('Name'),
* );
*
* $form['publication'] = array(
* '#type' => 'details',
* '#title' => t('Publication'),
* '#group' => 'information',
* );
*
* $form['publication']['publisher'] = array(
* '#type' => 'textfield',
* '#title' => t('Publisher'),
* );
* @endcode
*
* @FormElement("vertical_tabs")
*/

View file

@ -11,11 +11,23 @@ use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Processes attachments of HTML responses.
*
* This class is used by the rendering service to process the #attached part of
* the render array, for HTML responses.
*
* To render attachments to HTML for testing without a controller, use the
* 'bare_html_page_renderer' service to generate a
* Drupal\Core\Render\HtmlResponse object. Then use its getContent(),
* getStatusCode(), and/or the headers property to access the result.
*
* @see template_preprocess_html()
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
* @see \Drupal\Core\Render\BareHtmlPageRenderer
@ -66,6 +78,13 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
*/
protected $renderer;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a HtmlResponseAttachmentsProcessor object.
*
@ -81,14 +100,17 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The request stack.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*/
public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer) {
public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
$this->jsCollectionRenderer = $js_collection_renderer;
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
}
/**
@ -117,27 +139,65 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
return $e->getResponse();
}
// Get a reference to the attachments.
$attached = $response->getAttachments();
// Get the placeholders from attached and then remove them.
$attachment_placeholders = $attached['html_response_attachment_placeholders'];
unset($attached['html_response_attachment_placeholders']);
$variables = $this->processAssetLibraries($attached, $attachment_placeholders);
// Handle all non-asset attachments. This populates drupal_get_html_head().
$all_attached = ['#attached' => $attached];
drupal_process_attached($all_attached);
// Get HTML head elements - if present.
if (isset($attachment_placeholders['head'])) {
$variables['head'] = drupal_get_html_head(FALSE);
// Send a message back if the render array has unsupported #attached types.
$unsupported_types = array_diff(
array_keys($attached),
['html_head', 'feed', 'html_head_link', 'http_header', 'library', 'html_response_attachment_placeholders', 'placeholders', 'drupalSettings']
);
if (!empty($unsupported_types)) {
throw new \LogicException(sprintf('You are not allowed to use %s in #attached.', implode(', ', $unsupported_types)));
}
// Now replace the attachment placeholders.
$this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
// If we don't have any placeholders, there is no need to proceed.
if (!empty($attached['html_response_attachment_placeholders'])) {
// Get the placeholders from attached and then remove them.
$attachment_placeholders = $attached['html_response_attachment_placeholders'];
unset($attached['html_response_attachment_placeholders']);
// Finally set the headers on the response if any bubbled.
$variables = $this->processAssetLibraries($attached, $attachment_placeholders);
// Since we can only replace content in the HTML head section if there's a
// placeholder for it, we can safely avoid processing the render array if
// it's not present.
if (!empty($attachment_placeholders['head'])) {
// 'feed' is a special case of 'html_head_link'. We process them into
// 'html_head_link' entries and merge them.
if (!empty($attached['feed'])) {
$attached = BubbleableMetadata::mergeAttachments(
$attached,
$this->processFeed($attached['feed'])
);
}
// 'html_head_link' is a special case of 'html_head' which can be present
// as a head element, but also as a Link: HTTP header depending on
// settings in the render array. Processing it can add to both the
// 'html_head' and 'http_header' keys of '#attached', so we must address
// it before 'html_head'.
if (!empty($attached['html_head_link'])) {
// Merge the processed 'html_head_link' into $attached so that its
// 'html_head' and 'http_header' values are present for further
// processing.
$attached = BubbleableMetadata::mergeAttachments(
$attached,
$this->processHtmlHeadLink($attached['html_head_link'])
);
}
// Now we can process 'html_head', which contains both 'feed' and
// 'html_head_link'.
if (!empty($attached['html_head'])) {
$variables['head'] = $this->processHtmlHead($attached['html_head']);
}
}
// Now replace the attachment placeholders.
$this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
}
// Set the HTTP headers and status code on the response if any bubbled.
if (!empty($attached['http_header'])) {
$this->setHeaders($response, $attached['http_header']);
}
@ -175,7 +235,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
*/
protected function renderPlaceholders(HtmlResponse $response) {
$build = [
'#markup' => SafeString::create($response->getContent()),
'#markup' => Markup::create($response->getContent()),
'#attached' => $response->getAttachments(),
];
// RendererInterface::renderRoot() renders the $build render array and
@ -215,8 +275,7 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
// Take Ajax page state into account, to allow for something like Turbolinks
// to be implemented without altering core.
// @see https://github.com/rails/turbolinks/
// @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request.
$ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state');
$ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
$assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
$variables = [];
@ -243,6 +302,9 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
/**
* Renders HTML response attachment placeholders.
*
* This is the last step where all of the attachments are placed into the
* response object's contents.
*
* @param \Drupal\Core\Render\HtmlResponse $response
* The HTML response to update.
* @param array $placeholders
@ -268,7 +330,13 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* @param \Drupal\Core\Render\HtmlResponse $response
* The HTML response to update.
* @param array $headers
* The headers to set.
* The headers to set, as an array. The items in this array should be as
* follows:
* - The header name.
* - The header value.
* - (optional) Whether to replace a current value with the new one, or add
* it to the others. If the value is not replaced, it will be appended,
* resulting in a header like this: 'Header: value1,value2'
*/
protected function setHeaders(HtmlResponse $response, array $headers) {
foreach ($headers as $values) {
@ -281,8 +349,105 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
if (strtolower($name) === 'status') {
$response->setStatusCode($value);
}
$response->headers->set($name, $value, $replace);
else {
$response->headers->set($name, $value, $replace);
}
}
}
/**
* Ensure proper key/data order and defaults for renderable head items.
*
* @param array $html_head
* The ['#attached']['html_head'] portion of a render array.
*
* @return array
* The ['#attached']['html_head'] portion of a render array with #type of
* html_tag added for items without a #type.
*/
protected function processHtmlHead(array $html_head) {
$head = [];
foreach ($html_head as $item) {
list($data, $key) = $item;
if (!isset($data['#type'])) {
$data['#type'] = 'html_tag';
}
$head[$key] = $data;
}
return $head;
}
/**
* Transform a html_head_link array into html_head and http_header arrays.
*
* html_head_link is a special case of html_head which can be present as
* a link item in the HTML head section, and also as a Link: HTTP header,
* depending on options in the render array. Processing it can add to both the
* html_head and http_header sections.
*
* @param array $html_head_link
* The 'html_head_link' value of a render array. Each head link is specified
* by a two-element array:
* - An array specifying the attributes of the link.
* - A boolean specifying whether the link should also be a Link: HTTP
* header.
*
* @return array
* An ['#attached'] section of a render array. This allows us to easily
* merge the results with other render arrays. The array could contain the
* following keys:
* - http_header
* - html_head
*/
protected function processHtmlHeadLink(array $html_head_link) {
$attached = [];
foreach ($html_head_link as $item) {
$attributes = $item[0];
$should_add_header = isset($item[1]) ? $item[1] : FALSE;
$element = array(
'#tag' => 'link',
'#attributes' => $attributes,
);
$href = $attributes['href'];
$attached['html_head'][] = [$element, 'html_head_link:' . $attributes['rel'] . ':' . $href];
if ($should_add_header) {
// Also add a HTTP header "Link:".
$href = '<' . Html::escape($attributes['href'] . '>');
unset($attributes['href']);
$attached['http_header'][] = ['Link', $href . drupal_http_header_attributes($attributes), TRUE];
}
}
return $attached;
}
/**
* Transform a 'feed' attachment into an 'html_head_link' attachment.
*
* The RSS feed is a special case of 'html_head_link', so we just turn it into
* one.
*
* @param array $attached_feed
* The ['#attached']['feed'] portion of a render array.
*
* @return array
* An ['#attached']['html_head_link'] array, suitable for merging with
* another 'html_head_link' array.
*/
protected function processFeed($attached_feed) {
$html_head_link = [];
foreach($attached_feed as $item) {
$feed_link = [
'href' => $item[0],
'rel' => 'alternate',
'title' => empty($item[1]) ? '' : $item[1],
'type' => 'application/rss+xml',
];
$html_head_link[] = [$feed_link, FALSE];
}
return ['html_head_link' => $html_head_link];
}
}

View file

@ -12,6 +12,7 @@ use Drupal\Core\Cache\Cache;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Display\ContextAwareVariantInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
@ -192,11 +193,18 @@ class HtmlRenderer implements MainContentRendererInterface {
* If the selected display variant does not implement PageVariantInterface.
*/
protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) {
// Determine the title: use the title provided by the main content if any,
// otherwise get it from the routing information.
$get_title = function (array $main_content) use ($request, $route_match) {
return isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
};
// If the _controller result already is #type => page,
// we have no work to do: The "main content" already is an entire "page"
// (see html.html.twig).
if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
$page = $main_content;
$title = $get_title($page);
}
// Otherwise, render it as the main content of a #type => page, by selecting
// page display variant to do that and building that page display variant.
@ -228,6 +236,8 @@ class HtmlRenderer implements MainContentRendererInterface {
];
}
$title = $get_title($main_content);
// Instantiate the page display, and give it the main content.
$page_display = $this->displayVariantManager->createInstance($variant_id);
if (!$page_display instanceof PageVariantInterface) {
@ -235,7 +245,17 @@ class HtmlRenderer implements MainContentRendererInterface {
}
$page_display
->setMainContent($main_content)
->setTitle($title)
->addCacheableDependency($event)
->setConfiguration($event->getPluginConfiguration());
// Some display variants need to be passed an array of contexts with
// values because they can't get all their contexts globally. For example,
// in Page Manager, you can create a Page which has a specific static
// context (e.g. a context that refers to the Node with nid 6), if any
// such contexts were added to the $event, pass them to the $page_display.
if ($page_display instanceof ContextAwareVariantInterface) {
$page_display->setContexts($event->getContexts());
}
// Generate a #type => page render array using the page display variant,
// the page display will build the content for the various page regions.
@ -258,10 +278,6 @@ class HtmlRenderer implements MainContentRendererInterface {
// Allow hooks to add attachments to $page['#attached'].
$this->invokePageAttachmentHooks($page);
// Determine the title: use the title provided by the main content if any,
// otherwise get it from the routing information.
$title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
return [$page, $title];
}

View file

@ -2,13 +2,13 @@
/**
* @file
* Contains \Drupal\Core\Render\SafeString.
* Contains \Drupal\Core\Render\Markup.
*/
namespace Drupal\Core\Render;
use Drupal\Component\Utility\SafeStringInterface;
use Drupal\Component\Utility\SafeStringTrait;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\MarkupTrait;
/**
* Defines an object that passes safe strings through the render system.
@ -25,6 +25,6 @@ use Drupal\Component\Utility\SafeStringTrait;
* @see \Twig_Markup
* @see \Drupal\Component\Utility\SafeMarkup
*/
final class SafeString implements SafeStringInterface, \Countable {
use SafeStringTrait;
final class Markup implements MarkupInterface, \Countable {
use MarkupTrait;
}

View file

@ -114,17 +114,6 @@ class MetadataBubblingUrlGenerator implements UrlGeneratorInterface {
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}
*/

View file

@ -7,16 +7,26 @@
namespace Drupal\Core\Render;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Event fired when rendering main content, to select a page display variant.
*
* Subscribers of this event can call the following setters to pass additional
* information along to the selected variant:
* - self::setPluginConfiguration()
* - self::setContexts()
* - self::addCacheableDependency()
*
* @see \Drupal\Core\Render\RenderEvents::SELECT_PAGE_DISPLAY_VARIANT
* @see \Drupal\Core\Render\MainContent\HtmlRenderer
*/
class PageDisplayVariantSelectionEvent extends Event {
class PageDisplayVariantSelectionEvent extends Event implements RefinableCacheableDependencyInterface {
use RefinableCacheableDependencyTrait;
/**
* The selected page display variant plugin ID.
@ -39,6 +49,13 @@ class PageDisplayVariantSelectionEvent extends Event {
*/
protected $routeMatch;
/**
* An array of collected contexts to pass to the page display variant.
*
* @var \Drupal\Component\Plugin\Context\ContextInterface[]
*/
protected $contexts = [];
/**
* Constructs the page display variant plugin selection event.
*
@ -106,4 +123,27 @@ class PageDisplayVariantSelectionEvent extends Event {
return $this->routeMatch;
}
/**
* Gets the contexts that were set during event dispatch.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set contexts, keyed by context name.
*/
public function getContexts() {
return $this->contexts;
}
/**
* Sets the contexts to be passed to the page display variant.
*
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts, keyed by context name.
*
* @return $this
*/
public function setContexts(array $contexts) {
$this->contexts = $contexts;
return $this;
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy.
*/
namespace Drupal\Core\Render\Placeholder;
/**
* Renders placeholders using a chain of placeholder strategies.
*/
class ChainedPlaceholderStrategy implements PlaceholderStrategyInterface {
/**
* An ordered list of placeholder strategy services.
*
* Ordered according to service priority.
*
* @var \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface[]
*/
protected $placeholderStrategies = [];
/**
* Adds a placeholder strategy to use.
*
* @param \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface $strategy
* The strategy to add to the placeholder strategies.
*/
public function addPlaceholderStrategy(PlaceholderStrategyInterface $strategy) {
$this->placeholderStrategies[] = $strategy;
}
/**
* {@inheritdoc}
*/
public function processPlaceholders(array $placeholders) {
if (empty($placeholders)) {
return [];
}
// Assert that there is at least one strategy.
assert('!empty($this->placeholderStrategies)', 'At least one placeholder strategy must be present; by default the fallback strategy \Drupal\Core\Render\Placeholder\SingleFlushStrategy is always present.');
$new_placeholders = [];
// Give each placeholder strategy a chance to replace all not-yet replaced
// placeholders. The order of placeholder strategies is well defined
// and this uses a variation of the "chain of responsibility" design pattern.
foreach ($this->placeholderStrategies as $strategy) {
$processed_placeholders = $strategy->processPlaceholders($placeholders);
assert('array_intersect_key($processed_placeholders, $placeholders) === $processed_placeholders', 'Processed placeholders must be a subset of all placeholders.');
$placeholders = array_diff_key($placeholders, $processed_placeholders);
$new_placeholders += $processed_placeholders;
if (empty($placeholders)) {
break;
}
}
return $new_placeholders;
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface.
*/
namespace Drupal\Core\Render\Placeholder;
/**
* Provides an interface for defining a placeholder strategy service.
*/
interface PlaceholderStrategyInterface {
/**
* Processes placeholders to render them with different strategies.
*
* @param array $placeholders
* The placeholders to process, with the keys being the markup for the
* placeholders and the values the corresponding render array describing the
* data to be rendered.
*
* @return array
* The resulting placeholders, with a subset of the keys of $placeholders
* (and those being the markup for the placeholders) but with the
* corresponding render array being potentially modified to render e.g. an
* ESI or BigPipe placeholder.
*/
public function processPlaceholders(array $placeholders);
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder\SingleFlushStrategy
*/
namespace Drupal\Core\Render\Placeholder;
/**
* Defines the 'single_flush' placeholder strategy.
*
* This is designed to be the fallback strategy, so should have the lowest
* priority. All placeholders that are not yet replaced at this point will be
* rendered as is and delivered directly.
*/
class SingleFlushStrategy implements PlaceholderStrategyInterface {
/**
* {@inheritdoc}
*/
public function processPlaceholders(array $placeholders) {
// Return all placeholders as is; they should be rendered directly.
return $placeholders;
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder.
*/
namespace Drupal\Core\Render;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
/**
* Turns a render array into a placeholder.
*/
class PlaceholderGenerator implements PlaceholderGeneratorInterface {
/**
* The renderer configuration array.
*
* @var array
*/
protected $rendererConfig;
/**
* Constructs a new Placeholder service.
*
* @param array $renderer_config
* The renderer configuration array.
*/
public function __construct(array $renderer_config) {
$this->rendererConfig = $renderer_config;
}
/**
* {@inheritdoc}
*/
public function canCreatePlaceholder(array $element) {
return
// If generated by a #lazy_builder callback, placeholdering is possible.
isset($element['#lazy_builder'])
&&
// If #create_placeholder === FALSE, placeholdering is disallowed.
(!isset($element['#create_placeholder']) || $element['#create_placeholder'] !== FALSE);
}
/**
* {@inheritdoc}
*/
public function shouldAutomaticallyPlaceholder(array $element) {
$conditions = $this->rendererConfig['auto_placeholder_conditions'];
// Auto-placeholder if max-age is at or below the configured threshold.
if (isset($element['#cache']['max-age']) && $element['#cache']['max-age'] !== Cache::PERMANENT && $element['#cache']['max-age'] <= $conditions['max-age']) {
return TRUE;
}
// Auto-placeholder if a high-cardinality cache context is set.
if (isset($element['#cache']['contexts']) && array_intersect($element['#cache']['contexts'], $conditions['contexts'])) {
return TRUE;
}
// Auto-placeholder if a high-invalidation frequency cache tag is set.
if (isset($element['#cache']['tags']) && array_intersect($element['#cache']['tags'], $conditions['tags'])) {
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function createPlaceholder(array $element) {
$placeholder_render_array = array_intersect_key($element, [
// Placeholders are replaced with markup by executing the associated
// #lazy_builder callback, which generates a render array, and which the
// Renderer will render and replace the placeholder with.
'#lazy_builder' => TRUE,
// The cacheability metadata for the placeholder. The rendered result of
// the placeholder may itself be cached, if [#cache][keys] are specified.
'#cache' => TRUE,
]);
// Generate placeholder markup. Note that the only requirement is that this
// is unique markup that isn't easily guessable. The #lazy_builder callback
// and its arguments are put in the placeholder markup solely to simplify<<<
// debugging.
$callback = $placeholder_render_array['#lazy_builder'][0];
$arguments = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
$token = hash('crc32b', serialize($placeholder_render_array));
$placeholder_markup = '<drupal-render-placeholder callback="' . $callback . '" arguments="' . $arguments . '" token="' . $token . '"></drupal-render-placeholder>';
// Build the placeholder element to return.
$placeholder_element = [];
$placeholder_element['#markup'] = Markup::create($placeholder_markup);
$placeholder_element['#attached']['placeholders'][$placeholder_markup] = $placeholder_render_array;
return $placeholder_element;
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\PlaceholderGeneratorInterface.
*/
namespace Drupal\Core\Render;
/**
* Defines an interface for turning a render array into a placeholder.
*
* This encapsulates logic related to generating placeholders.
*
* Makes it possible to determine whether a render array can be placeholdered
* (it can be reconstructed independently of the request context), whether a
* render array should be placeholdered (its cacheability meets the conditions),
* and to create a placeholder.
*
* @see \Drupal\Core\Render\RendererInterface
*/
interface PlaceholderGeneratorInterface {
/**
* Analyzes whether the given render array can be placeholdered.
*
* @param array $element
* A render array. Its #lazy_builder and #create_placeholder properties are
* analyzed.
*
* @return bool
*/
public function canCreatePlaceholder(array $element);
/**
* Whether the given render array should be automatically placeholdered.
*
* @param array $element
* The render array whose cacheability to analyze.
*
* @return bool
* Whether the given render array's cacheability meets the placeholdering
* conditions.
*/
public function shouldAutomaticallyPlaceholder(array $element);
/**
* Turns the given element into a placeholder.
*
* Placeholdering allows us to avoid "poor cacheability contamination": this
* maps the current render array to one that only has #markup and #attached,
* and #attached contains a placeholder with this element's prior cacheability
* metadata. In other words: this placeholder is perfectly cacheable, the
* placeholder replacement logic effectively cordons off poor cacheability.
*
* @param array $element
* The render array to create a placeholder for.
*
* @return array
* Render array with placeholder markup and the attached placeholder
* replacement metadata.
*/
public function createPlaceholder(array $element);
}

View file

@ -0,0 +1,183 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\PlaceholderingRenderCache.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Cache\CacheFactoryInterface;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Adds automatic placeholdering to the RenderCache.
*
* This automatic placeholdering is performed to ensure the containing elements
* and overarching response are as cacheable as possible. Elements whose subtree
* bubble either max-age=0 or high-cardinality cache contexts (such as 'user'
* and 'session') are considered poorly cacheable.
*
* @see sites/default/default.services.yml
*
* Automatic placeholdering is performed only on elements whose subtree was
* generated using a #lazy_builder callback and whose bubbled cacheability meets
* the auto-placeholdering conditions as configured in the renderer.config
* container parameter.
*
* This RenderCache implementation automatically replaces an element with a
* placeholder:
* - on render cache hit, i.e. ::get()
* - on render cache miss, i.e. ::set() (in subsequent requests, it will be a
* cache hit)
*
* In either case, the render cache is guaranteed to contain the to-be-rendered
* placeholder, so replacing (rendering) the placeholder will be very fast.
*
* Finally, in case the render cache item disappears between the time it is
* decided to automatically placeholder the element and the time where the
* placeholder is replaced (rendered), that is guaranteed to not be problematic.
* Because this only automatically placeholders elements that have a
* #lazy_builder callback set, which means that in the worst case, it will need
* to be re-rendered.
*/
class PlaceholderingRenderCache extends RenderCache {
/**
* The placeholder generator.
*
* @var \Drupal\Core\Render\PlaceholderGeneratorInterface
*/
protected $placeholderGenerator;
/**
* Stores rendered results for automatically placeholdered elements.
*
* This allows us to avoid talking to the cache twice per auto-placeholdered
* element, or in case of an uncacheable element, to render it twice.
*
* Scenario A. The double cache read would happen because:
* 1. when rendering, cache read, but auto-placeholdered
* 2. when rendering placeholders, again cache read
*
* Scenario B. The cache write plus read would happen because:
* 1. when rendering, cache write, but auto-placeholdered
* 2. when rendering placeholders, cache read
*
* Scenario C. The double rendering for an uncacheable element would happen because:
* 1. when rendering, not cacheable, but auto-placeholdered
* 2. when rendering placeholders, rendered again
*
* In all three scenarios, this static cache avoids the second step, thus
* avoiding expensive work.
*
* @var array
*/
protected $placeholderResultsCache = [];
/**
* Constructs a new PlaceholderingRenderCache object.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory
* The cache factory.
* @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
* The cache contexts manager.
* @param \Drupal\Core\Render\PlaceholderGeneratorInterface $placeholder_generator
* The placeholder generator.
*/
public function __construct(RequestStack $request_stack, CacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager, PlaceholderGeneratorInterface $placeholder_generator) {
parent::__construct($request_stack, $cache_factory, $cache_contexts_manager);
$this->placeholderGenerator = $placeholder_generator;
}
/**
* {@inheritdoc}
*/
public function get(array $elements) {
// When rendering placeholders, special case auto-placeholdered elements:
// avoid retrieving them from cache again, or rendering them again.
if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === FALSE) {
$cached_placeholder_result = $this->getFromPlaceholderResultsCache($elements);
if ($cached_placeholder_result !== FALSE) {
return $cached_placeholder_result;
}
}
$cached_element = parent::get($elements);
if ($cached_element === FALSE) {
return FALSE;
}
else {
if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($cached_element)) {
return $this->createPlaceholderAndRemember($cached_element, $elements);
}
return $cached_element;
}
}
/**
* {@inheritdoc}
*/
public function set(array &$elements, array $pre_bubbling_elements) {
$result = parent::set($elements, $pre_bubbling_elements);
if ($this->placeholderGenerator->canCreatePlaceholder($pre_bubbling_elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) {
// Overwrite $elements with a placeholder. The Renderer (which called this
// method) will update the context with the bubbleable metadata of the
// overwritten $elements.
$elements = $this->createPlaceholderAndRemember($this->getCacheableRenderArray($elements), $pre_bubbling_elements);
}
return $result;
}
/**
* Create a placeholder for a renderable array and remember in a static cache.
*
* @param array $rendered_elements
* A fully rendered renderable array.
* @param array $pre_bubbling_elements
* A renderable array corresponding to the state (in particular, the
* cacheability metadata) of $rendered_elements prior to the beginning of
* its rendering process, and therefore before any bubbling of child
* information has taken place. Only the #cache property is used by this
* function, so the caller may omit all other properties and children from
* this array.
*
* @return array
* Renderable array with placeholder markup and the attached placeholder
* replacement metadata.
*/
protected function createPlaceholderAndRemember(array $rendered_elements, array $pre_bubbling_elements) {
$placeholder_element = $this->placeholderGenerator->createPlaceholder($pre_bubbling_elements);
// Remember the result for this placeholder to avoid double work.
$placeholder = (string) $placeholder_element['#markup'];
$this->placeholderResultsCache[$placeholder] = $rendered_elements;
return $placeholder_element;
}
/**
* Retrieves an auto-placeholdered renderable array from the static cache.
*
* @param array $elements
* A renderable array.
*
* @return array|false
* A renderable array, with the original element and all its children pre-
* rendered, or FALSE if no cached copy of the element is available.
*/
protected function getFromPlaceholderResultsCache(array $elements) {
$placeholder_element = $this->placeholderGenerator->createPlaceholder($elements);
$placeholder = (string) $placeholder_element['#markup'];
if (isset($this->placeholderResultsCache[$placeholder])) {
return $this->placeholderResultsCache[$placeholder];
}
return FALSE;
}
}

View file

@ -27,6 +27,13 @@ class SimplePageVariant extends VariantBase implements PageVariantInterface {
*/
protected $mainContent;
/**
* The page title: a string (plain title) or a render array (formatted title).
*
* @var string|array
*/
protected $title = '';
/**
* {@inheritdoc}
*/
@ -35,17 +42,30 @@ class SimplePageVariant extends VariantBase implements PageVariantInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function setTitle($title) {
$this->title = $title;
return $this;
}
/**
* {@inheritdoc}
*/
public function build() {
$build = [
'content' => [
'main_content' => $this->mainContent,
'messages' => [
'#type' => 'status_messages',
'#weight' => -1000,
],
'page_title' => [
'#type' => 'page_title',
'#title' => $this->title,
'#weight' => -900,
],
'main_content' => ['#weight' => -800] + $this->mainContent,
],
];
return $build;

View file

@ -16,6 +16,11 @@ use Symfony\Component\HttpFoundation\RequestStack;
/**
* Wraps the caching logic for the render caching system.
*
* @internal
*
* @todo Refactor this out into a generic service capable of cache redirects,
* and let RenderCache use that. https://www.drupal.org/node/2551419
*/
class RenderCache implements RenderCacheInterface {
@ -336,10 +341,10 @@ class RenderCache implements RenderCacheInterface {
// the cache entry size.
if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) {
$data['#cache_properties'] = $elements['#cache_properties'];
// Ensure that any safe strings are a SafeString object.
// Ensure that any safe strings are a Markup object.
foreach (Element::properties(array_flip($elements['#cache_properties'])) as $cache_property) {
if (isset($elements[$cache_property]) && is_scalar($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) {
$elements[$cache_property] = SafeString::create($elements[$cache_property]);
$elements[$cache_property] = Markup::create($elements[$cache_property]);
}
}
@ -352,13 +357,13 @@ class RenderCache implements RenderCacheInterface {
// Cache only cacheable children's markup.
foreach ($cacheable_children as $key) {
// We can assume that #markup is safe at this point.
$cacheable_items[$key] = ['#markup' => SafeString::create($cacheable_items[$key]['#markup'])];
$cacheable_items[$key] = ['#markup' => Markup::create($cacheable_items[$key]['#markup'])];
}
}
$data += $cacheable_items;
}
$data['#markup'] = SafeString::create($data['#markup']);
$data['#markup'] = Markup::create($data['#markup']);
return $data;
}

View file

@ -10,8 +10,9 @@ namespace Drupal\Core\Render;
/**
* Defines an interface for caching rendered render arrays.
*
* @see sec_caching
* @internal
*
* @see sec_caching
* @see \Drupal\Core\Render\RendererInterface
*/
interface RenderCacheInterface {

View file

@ -0,0 +1,23 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\RenderableInterface.
*/
namespace Drupal\Core\Render;
/**
* Defines an object which can be rendered by the Render API.
*/
interface RenderableInterface {
/**
* Returns a render array representation of the object.
*
* @return mixed[]
* A render array.
*/
public function toRenderable();
}

View file

@ -9,13 +9,11 @@ namespace Drupal\Core\Render;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\Cache;
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;
@ -45,6 +43,13 @@ class Renderer implements RendererInterface {
*/
protected $elementInfo;
/**
* The placeholder generator.
*
* @var \Drupal\Core\Render\PlaceholderGeneratorInterface
*/
protected $placeholderGenerator;
/**
* The render cache service.
*
@ -99,6 +104,8 @@ class Renderer implements RendererInterface {
* The theme manager.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info.
* @param \Drupal\Core\Render\PlaceholderGeneratorInterface $placeholder_generator
* The placeholder generator.
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
* The render cache service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
@ -106,10 +113,11 @@ class Renderer implements RendererInterface {
* @param array $renderer_config
* The renderer configuration array.
*/
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, PlaceholderGeneratorInterface $placeholder_generator, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
$this->controllerResolver = $controller_resolver;
$this->theme = $theme;
$this->elementInfo = $element_info;
$this->placeholderGenerator = $placeholder_generator;
$this->renderCache = $render_cache;
$this->rendererConfig = $renderer_config;
$this->requestStack = $request_stack;
@ -179,7 +187,7 @@ class Renderer implements RendererInterface {
// Replace the placeholder with its rendered markup, and merge its
// bubbleable metadata with the main elements'.
$elements['#markup'] = SafeString::create(str_replace($placeholder, $markup, $elements['#markup']));
$elements['#markup'] = Markup::create(str_replace($placeholder, $markup, $elements['#markup']));
$elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements);
// Remove the placeholder that we've just rendered.
@ -284,7 +292,7 @@ class Renderer implements RendererInterface {
}
// Mark the element markup as safe if is it a string.
if (is_string($elements['#markup'])) {
$elements['#markup'] = SafeString::create($elements['#markup']);
$elements['#markup'] = Markup::create($elements['#markup']);
}
// The render cache item contains all the bubbleable rendering metadata
// for the subtree.
@ -295,12 +303,15 @@ class Renderer implements RendererInterface {
return $elements['#markup'];
}
}
// Two-tier caching: track pre-bubbling elements' #cache for later
// comparison.
// Two-tier caching: track pre-bubbling elements' #cache, #lazy_builder and
// #create_placeholder 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'] : [];
$pre_bubbling_elements = array_intersect_key($elements, [
'#cache' => TRUE,
'#lazy_builder' => TRUE,
'#create_placeholder' => TRUE,
]);
// If the default values for this element have not been loaded yet, populate
// them.
@ -342,7 +353,7 @@ class Renderer implements RendererInterface {
}
}
// Determine whether to do auto-placeholdering.
if (isset($elements['#lazy_builder']) && (!isset($elements['#create_placeholder']) || $elements['#create_placeholder'] !== FALSE) && $this->shouldAutomaticallyPlaceholder($elements)) {
if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) {
$elements['#create_placeholder'] = TRUE;
}
// If instructed to create a placeholder, and a #lazy_builder callback is
@ -352,7 +363,7 @@ class Renderer implements RendererInterface {
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);
$elements = $this->placeholderGenerator->createPlaceholder($elements);
}
// Build the element if it is still empty.
if (isset($elements['#lazy_builder'])) {
@ -455,7 +466,7 @@ class Renderer implements RendererInterface {
foreach ($children as $key) {
$elements['#children'] .= $this->doRender($elements[$key]);
}
$elements['#children'] = SafeString::create($elements['#children']);
$elements['#children'] = Markup::create($elements['#children']);
}
// If #theme is not implemented and the element has raw #markup as a
@ -466,7 +477,7 @@ class Renderer implements RendererInterface {
// 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'] = SafeString::create($elements['#markup'] . $elements['#children']);
$elements['#children'] = Markup::create($elements['#markup'] . $elements['#children']);
}
// Let the theme functions in #theme_wrappers add markup around the rendered
@ -519,7 +530,7 @@ class Renderer implements RendererInterface {
$prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
$suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
$elements['#markup'] = SafeString::create($prefix . $elements['#children'] . $suffix);
$elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix);
// We've rendered this element (and its subtree!), now update the context.
$context->update($elements);
@ -531,6 +542,12 @@ class Renderer implements RendererInterface {
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);
// Update the render context; the render cache implementation may update
// the element, and it may have different bubbleable metadata now.
// @see \Drupal\Core\Render\PlaceholderingRenderCache::set()
$context->pop();
$context->push(new BubbleableMetadata());
$context->update($elements);
}
// Only when we're in a root (non-recursive) Renderer::render() call,
@ -643,81 +660,6 @@ class Renderer implements RendererInterface {
return TRUE;
}
/**
* Whether the given render array should be automatically placeholdered.
*
* @param array $element
* The render array whose cacheability to analyze.
*
* @return bool
* Whether the given render array's cacheability meets the placeholdering
* conditions.
*/
protected function shouldAutomaticallyPlaceholder(array $element) {
$conditions = $this->rendererConfig['auto_placeholder_conditions'];
// Auto-placeholder if max-age is at or below the configured threshold.
if (isset($element['#cache']['max-age']) && $element['#cache']['max-age'] !== Cache::PERMANENT && $element['#cache']['max-age'] <= $conditions['max-age']) {
return TRUE;
}
// Auto-placeholder if a high-cardinality cache context is set.
if (isset($element['#cache']['contexts']) && array_intersect($element['#cache']['contexts'], $conditions['contexts'])) {
return TRUE;
}
// Auto-placeholder if a high-invalidation frequency cache tag is set.
if (isset($element['#cache']['tags']) && array_intersect($element['#cache']['tags'], $conditions['tags'])) {
return TRUE;
}
return FALSE;
}
/**
* Turns this element into a placeholder.
*
* Placeholdering allows us to avoid "poor cacheability contamination": this
* maps the current render array to one that only has #markup and #attached,
* and #attached contains a placeholder with this element's prior cacheability
* metadata. In other words: this placeholder is perfectly cacheable, the
* placeholder replacement logic effectively cordons off poor cacheability.
*
* @param array $element
* The render array to create a placeholder for.
*
* @return array
* Render array with placeholder markup and the attached placeholder
* replacement metadata.
*/
protected function createPlaceholder(array $element) {
$placeholder_render_array = array_intersect_key($element, [
// Placeholders are replaced with markup by executing the associated
// #lazy_builder callback, which generates a render array, and which the
// Renderer will render and replace the placeholder with.
'#lazy_builder' => TRUE,
// The cacheability metadata for the placeholder. The rendered result of
// the placeholder may itself be cached, if [#cache][keys] are specified.
'#cache' => TRUE,
]);
// Generate placeholder markup. Note that the only requirement is that this
// is unique markup that isn't easily guessable. The #lazy_builder callback
// and its arguments are put in the placeholder markup solely to simplify
// debugging.
$attributes = new Attribute();
$attributes['callback'] = $placeholder_render_array['#lazy_builder'][0];
$attributes['arguments'] = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
$attributes['token'] = hash('crc32b', serialize($placeholder_render_array));
$placeholder_markup = SafeMarkup::format('<drupal-render-placeholder@attributes></drupal-render-placeholder>', ['@attributes' => $attributes]);
// Build the placeholder element to return.
$placeholder_element = [];
$placeholder_element['#markup'] = $placeholder_markup;
$placeholder_element['#attached']['placeholders'][$placeholder_markup] = $placeholder_render_array;
return $placeholder_element;
}
/**
* {@inheritdoc}
*/
@ -743,18 +685,18 @@ class Renderer implements RendererInterface {
* Note: This method only filters if $string is not marked safe already. This
* ensures that HTML intended for display is not filtered.
*
* @param string|\Drupal\Core\Render\SafeString $string
* @param string|\Drupal\Core\Render\Markup $string
* A string.
*
* @return \Drupal\Core\Render\SafeString
* The escaped string wrapped in a SafeString object. If
* @return \Drupal\Core\Render\Markup
* The escaped string wrapped in a Markup object. If
* SafeMarkup::isSafe($string) returns TRUE, it won't be escaped again.
*/
protected function xssFilterAdminIfUnsafe($string) {
if (!SafeMarkup::isSafe($string)) {
$string = Xss::filterAdmin($string);
}
return SafeString::create($string);
return Markup::create($string);
}
/**
@ -775,8 +717,8 @@ class Renderer implements RendererInterface {
* @param array $elements
* A render array with #markup set.
*
* @return \Drupal\Component\Utility\SafeStringInterface|string
* The escaped markup wrapped in a SafeString object. If
* @return \Drupal\Component\Render\MarkupInterface|string
* The escaped markup wrapped in a Markup object. If
* SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be
* escaped or filtered again.
*
@ -790,12 +732,12 @@ class Renderer implements RendererInterface {
}
if (!empty($elements['#plain_text'])) {
$elements['#markup'] = SafeString::create(Html::escape($elements['#plain_text']));
$elements['#markup'] = Markup::create(Html::escape($elements['#plain_text']));
}
elseif (!SafeMarkup::isSafe($elements['#markup'])) {
// The default behaviour is to XSS filter using the admin tag list.
$tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
$elements['#markup'] = SafeString::create(Xss::filter($elements['#markup'], $tags));
$elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags));
}
return $elements;

View file

@ -27,7 +27,7 @@ interface RendererInterface {
* @param array $elements
* The structured array describing the data to be rendered.
*
* @return \Drupal\Component\Utility\SafeStringInterface
* @return \Drupal\Component\Render\MarkupInterface
* The rendered HTML.
*
* @see ::render()
@ -42,7 +42,7 @@ interface RendererInterface {
*
* Calls ::render() in such a way that placeholders are replaced.
*
* Useful for e.g. rendering the values of tokens or e-mails, which need a
* Useful for e.g. rendering the values of tokens or emails, which need a
* render array being turned into a string, but don't need any of the
* bubbleable metadata (the attached assets the cache tags).
*
@ -58,7 +58,7 @@ interface RendererInterface {
* @param array $elements
* The structured array describing the data to be rendered.
*
* @return \Drupal\Component\Utility\SafeStringInterface
* @return \Drupal\Component\Render\MarkupInterface
* The rendered HTML.
*
* @see ::renderRoot()
@ -209,7 +209,7 @@ interface RendererInterface {
* drupal_process_states().
* - If this element has #attached defined then any required libraries,
* JavaScript, CSS, or other custom data are added to the current page by
* drupal_process_attached().
* \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments().
* - If this element has an array of #theme_wrappers defined and
* #render_children is not set, #children is then re-rendered by passing
* the element in its current state to ThemeManagerInterface::render()
@ -302,7 +302,7 @@ interface RendererInterface {
* (Internal use only.) Whether this is a recursive call or not. See
* ::renderRoot().
*
* @return \Drupal\Component\Utility\SafeStringInterface
* @return \Drupal\Component\Render\MarkupInterface
* The rendered HTML.
*
* @throws \LogicException
@ -316,7 +316,7 @@ interface RendererInterface {
* @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo()
* @see \Drupal\Core\Theme\ThemeManagerInterface::render()
* @see drupal_process_states()
* @see drupal_process_attached()
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
* @see ::renderRoot()
*/
public function render(&$elements, $is_root_call = FALSE);

View file

@ -70,7 +70,10 @@
* hook_theme() implementations can also specify that a theme hook
* implementation is a theme function, but that is uncommon. It is only used for
* special cases, for performance reasons, because rendering using theme
* functions is somewhat faster than theme templates.
* functions is somewhat faster than theme templates. Note that while Twig
* templates will auto-escape variables, theme functions must explicitly escape
* any variables by using theme_render_and_autoescape(). Failure to do so is
* likely to result in security vulnerabilities.
*
* @section sec_overriding_theme_hooks Overriding Theme Hooks
* Themes may register new theme hooks within a hook_theme() implementation, but
@ -93,6 +96,9 @@
* bartik_search_result() in the bartik.theme file, if the search_result hook
* implementation was a function instead of a template). Normally, copying the
* default function is again a good starting point for overriding its behavior.
* Again, note that theme functions (unlike templates) must explicitly escape
* variables using theme_render_and_autoescape() or risk security
* vulnerabilities.
*
* @section sec_preprocess_templates Preprocessing for Template Files
* If the theme implementation is a template file, several functions are called
@ -375,7 +381,10 @@
* Libraries, JavaScript settings, feeds, HTML <head> tags and HTML <head> links
* are attached to elements using the #attached property. The #attached property
* is an associative array, where the keys are the attachment types and the
* values are the attached data. For example:
* values are the attached data.
*
* The #attached property can also be used to specify HTTP headers and the
* response status code.
*
* The #attached property allows loading of asset libraries (which may contain
* CSS assets, JavaScript assets, and JavaScript setting assets), JavaScript
@ -386,10 +395,11 @@
* @code
* $build['#attached']['library'][] = 'core/jquery';
* $build['#attached']['drupalSettings']['foo'] = 'bar';
* $build['#attached']['feed'][] = ['aggregator/rss', $this->t('Feed title')];
* $build['#attached']['feed'][] = [$url, $this->t('Feed title')];
* @endcode
*
* See drupal_process_attached() for additional information.
* See \Drupal\Core\Render\AttachmentsResponseProcessorInterface for additional
* information.
*
* See \Drupal\Core\Asset\LibraryDiscoveryParser::parseLibraryInfo() for more
* information on how to define libraries.