Commit b544422d authored by catch's avatar catch

Issue #2477223 by Mile23, Wim Leers, j2r, ianthomas_uk, jaimeguzman, pjbaert:...

Issue #2477223 by Mile23, Wim Leers, j2r, ianthomas_uk, jaimeguzman, pjbaert: Refactor _drupal_add_html_head, drupal_get_html_head, _drupal_add_html_head_link into the attachments processor, remove from common.inc
parent 85042d01
......@@ -1059,7 +1059,7 @@ services:
html_response.attachments_processor:
class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor
tags:
arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer']
arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
html_response.subscriber:
class: Drupal\Core\EventSubscriber\HtmlResponseSubscriber
tags:
......
......@@ -147,62 +147,6 @@
*/
const LOCALE_PLURAL_DELIMITER = "\03";
/**
* Adds output to the HEAD tag of the HTML page.
*
* This function can be called as long as the headers aren't sent. Pass no
* arguments (or NULL for both) to retrieve the currently stored elements.
*
* @param $data
* A renderable array. If the '#type' key is not set then 'html_tag' will be
* added as the default '#type'.
* @param $key
* A unique string key to allow implementations of hook_html_head_alter() to
* identify the element in $data. Required if $data is not NULL.
*
* @return
* An array of all stored HEAD elements.
*
* @see \Drupal\Core\Render\Element\HtmlTag::preRenderHtmlTag()
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 8.0.0
* Use #attached on render arrays.
*/
function _drupal_add_html_head($data = NULL, $key = NULL) {
$stored_head = &drupal_static(__FUNCTION__, array());
if (isset($data) && isset($key)) {
if (!isset($data['#type'])) {
$data['#type'] = 'html_tag';
}
$stored_head[$key] = $data;
}
return $stored_head;
}
/**
* Retrieves output to be displayed in the HEAD tag of the HTML page.
*
* @param bool $render
* If TRUE render the HEAD elements, otherwise return just the elements.
*
* @return string|array
* Return the rendered HTML head or the elements itself.
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 8.0.0
* Use #attached on render arrays.
*/
function drupal_get_html_head($render = TRUE) {
$elements = _drupal_add_html_head();
\Drupal::moduleHandler()->alter('html_head', $elements);
if ($render) {
return \Drupal::service('renderer')->renderPlain($elements);
}
else {
return $elements;
}
}
/**
* Prepares a 'destination' URL query parameter for use with url().
*
......@@ -476,39 +420,6 @@ function base_path() {
return $GLOBALS['base_path'];
}
/**
* Adds a LINK tag with a distinct 'rel' attribute to the page's HEAD.
*
* This function can be called as long the HTML header hasn't been sent, which
* on normal pages is up through the preprocess step of _theme('html'). Adding
* a link will overwrite a prior link with the exact same 'rel' and 'href'
* attributes.
*
* @param $attributes
* Associative array of element attributes including 'href' and 'rel'.
* @param $header
* Optional flag to determine if a HTTP 'Link:' header should be sent.
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 8.0.0
* Use #attached on render arrays.
*/
function _drupal_add_html_head_link($attributes, $header = FALSE) {
$element = array(
'#tag' => 'link',
'#attributes' => $attributes,
);
$href = $attributes['href'];
if ($header) {
// Also add a HTTP header "Link:".
$href = '<' . Html::escape($attributes['href']) . '>;';
unset($attributes['href']);
$element['#attached']['http_header'][] = array('Link', $href . drupal_http_header_attributes($attributes), TRUE);
}
_drupal_add_html_head($element, 'html_head_link:' . $attributes['rel'] . ':' . $href);
}
/**
* Deletes old cached CSS files.
*
......@@ -607,39 +518,8 @@ function drupal_js_defaults($data = NULL) {
* 'bare_html_page_renderer' service.
*/
function drupal_process_attached(array $elements) {
// Asset attachments are handled by \Drupal\Core\Asset\AssetResolver.
foreach (array('library', 'drupalSettings') as $type) {
unset($elements['#attached'][$type]);
}
// Add additional types of attachments specified in the render() structure.
foreach ($elements['#attached'] as $callback => $options) {
foreach ($elements['#attached'][$callback] as $args) {
// Limit the amount allowed entries.
switch ($callback) {
case 'html_head':
call_user_func_array('_drupal_add_html_head', $args);
break;
case 'feed':
$args = [[
'href' => $args[0],
'rel' => 'alternate',
'title' => $args[1],
'type' => 'application/rss+xml',
]];
call_user_func_array('_drupal_add_html_head_link', $args);
break;
case 'html_head_link':
call_user_func_array('_drupal_add_html_head_link', $args);
break;
case 'http_header':
// @todo Remove validation in https://www.drupal.org/node/2477223
break;
default:
throw new \LogicException(sprintf('You are not allowed to use %s in #attached', $callback));
}
}
}
$build['#attached'] = $elements['#attached'];
\Drupal::service('renderer')->render($build);
}
/**
......
......@@ -9,6 +9,8 @@
/**
* 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);
......
......@@ -11,11 +11,23 @@
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,67 @@ public function processAttachments(AttachmentsInterface $response) {
return $e->getResponse();
}
// Get a reference to the attachments.
$attached = $response->getAttachments();
// 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)));
}
// 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);
// 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'])
);
}
// Get HTML head elements - if present.
if (isset($attachment_placeholders['head'])) {
$variables['head'] = drupal_get_html_head(FALSE);
// Now we can process 'html_head', which contains both 'feed' and
// 'html_head_link'.
if (!empty($attached['html_head'])) {
$html_head = $this->processHtmlHead($attached['html_head']);
// Invoke hook_html_head_alter().
$this->moduleHandler->alter('html_head', $html_head);
// Store the result in $variables so it can be inserted into the
// placeholder.
$variables['head'] = $html_head;
}
}
// Now replace the attachment placeholders.
$this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
// Finally set the headers on the response if any bubbled.
// Set the HTTP headers and status code on the response if any bubbled.
if (!empty($attached['http_header'])) {
$this->setHeaders($response, $attached['http_header']);
}
......@@ -243,6 +305,9 @@ protected function processAssetLibraries(array $attached, array $placeholders) {
/**
* 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 +333,13 @@ protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $respon
* @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 +352,105 @@ protected function setHeaders(HtmlResponse $response, array $headers) {
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];
}
}
......@@ -375,7 +375,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 +389,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.
......
......@@ -466,7 +466,6 @@ function template_preprocess_book_export_html(&$variables) {
$variables['base_url'] = $base_url;
$variables['language'] = $language_interface;
$variables['language_rtl'] = ($language_interface->getDirection() == LanguageInterface::DIRECTION_RTL);
$variables['head'] = drupal_get_html_head();
// HTML element attributes.
$attributes = array();
......
......@@ -586,7 +586,6 @@ protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperI
*/
protected function render(array &$elements) {
$content = $this->container->get('renderer')->renderRoot($elements);
drupal_process_attached($elements);
$this->setRawContent($content);
$this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));
return $content;
......
......@@ -68,7 +68,6 @@ function testBasicFeedAddNoTitle() {
);
// Glean the content from the response object.
$this->setRawContent($response->getContent());
// Assert that the content contains the RSS links we specified.
foreach ($urls as $description => $feed_info) {
$this->assertPattern($this->urlToRSSLinkPattern($feed_info['url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description)));
......
......@@ -55,12 +55,13 @@ function testDrupalRenderThemePreprocessAttached() {
/**
* Tests that we get an exception when we try to attach an illegal type.
*/
public function testDrupalProcessAttached() {
public function testProcessAttached() {
// Specify invalid attachments in a render array.
$build['#attached']['library'][] = 'core/drupal.states';
$build['#attached']['drupal_process_states'][] = [];
$renderer = $this->container->get('bare_html_page_renderer');
try {
$this->render($build);
$renderer->renderBarePage($build, '', $this->container->get('theme.manager')->getActiveTheme()->getName());
$this->fail("Invalid #attachment 'drupal_process_states' allowed");
}
catch (\LogicException $e) {
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\HttpKernel\HeadersResponseCodeRenderTest.
*/
namespace Drupal\system\Tests\HttpKernel;
use Drupal\simpletest\WebTestBase;
/**
* Tests rendering headers and response codes.
*
* @group Routing
*/
class HeadersResponseCodeRenderTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('httpkernel_test');
/**
* Tests the rendering of an array-based header and response code.
*/
public function testHeaderResponseCode() {
$this->drupalGet('/httpkernel-test/teapot');
$this->assertResponse(418);
$this->assertHeader('X-Test-Teapot', 'Teapot Mode Active');
$this->assertHeader('X-Test-Teapot-Replace', 'Teapot replaced');
$this->assertHeader('X-Test-Teapot-No-Replace', 'This value is not replaced,This one is added');
}
}
......@@ -4,9 +4,3 @@ httpkernel_test.empty:
_controller: '\Drupal\httpkernel_test\Controller\TestController::get'
requirements:
_access: 'TRUE'
httpkernel_test.teapot:
path: '/httpkernel-test/teapot'
defaults:
_controller: '\Drupal\httpkernel_test\Controller\TestController::teapot'
requirements:
_access: 'TRUE'
......@@ -21,21 +21,4 @@ public function get() {
return new Response();
}
/**
* Test special header and status code rendering.
*
* @return array
* A render array using features of the 'http_header' directive.
*/
public function teapot() {
$render = [];
$render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'This value gets replaced'];
$render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'Teapot replaced', TRUE];
$render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This value is not replaced'];
$render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This one is added', FALSE];
$render['#attached']['http_header'][] = ['X-Test-Teapot', 'Teapot Mode Active'];
$render['#attached']['http_header'][] = ['Status', "418 I'm a teapot."];
return $render;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment