Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 146 |
HtmlResponseAttachmentsProcessor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 9 |
1332 | |
0.00% |
0 / 146 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 8 |
|||
processAttachments | |
0.00% |
0 / 1 |
132 | |
0.00% |
0 / 53 |
|||
renderPlaceholders | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 12 |
|||
processAssetLibraries | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 13 |
|||
renderHtmlResponseAttachmentPlaceholders | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 8 |
|||
setHeaders | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
|||
processHtmlHead | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 10 |
|||
processHtmlHeadLink | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 18 |
|||
processFeed | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 12 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Render\HtmlResponseAttachmentsProcessor. | |
*/ | |
namespace Drupal\Core\Render; | |
use Drupal\Core\Asset\AssetCollectionRendererInterface; | |
use Drupal\Core\Asset\AssetResolverInterface; | |
use Drupal\Core\Asset\AttachedAssets; | |
use Drupal\Core\Asset\AttachedAssetsInterface; | |
use Drupal\Core\Config\ConfigFactoryInterface; | |
use Drupal\Core\Form\EnforcedResponseException; | |
use Drupal\Core\Extension\ModuleHandlerInterface; | |
use Drupal\Component\Utility\Html; | |
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 | |
* @see \Drupal\Core\Render\HtmlResponse | |
* @see \Drupal\Core\Render\MainContent\HtmlRenderer | |
*/ | |
class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface { | |
/** | |
* The asset resolver service. | |
* | |
* @var \Drupal\Core\Asset\AssetResolverInterface | |
*/ | |
protected $assetResolver; | |
/** | |
* A config object for the system performance configuration. | |
* | |
* @var \Drupal\Core\Config\Config | |
*/ | |
protected $config; | |
/** | |
* The CSS asset collection renderer service. | |
* | |
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface | |
*/ | |
protected $cssCollectionRenderer; | |
/** | |
* The JS asset collection renderer service. | |
* | |
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface | |
*/ | |
protected $jsCollectionRenderer; | |
/** | |
* The request stack. | |
* | |
* @var \Symfony\Component\HttpFoundation\RequestStack | |
*/ | |
protected $requestStack; | |
/** | |
* The renderer. | |
* | |
* @var \Drupal\Core\Render\RendererInterface | |
*/ | |
protected $renderer; | |
/** | |
* The module handler service. | |
* | |
* @var \Drupal\Core\Extension\ModuleHandlerInterface | |
*/ | |
protected $moduleHandler; | |
/** | |
* Constructs a HtmlResponseAttachmentsProcessor object. | |
* | |
* @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver | |
* An asset resolver. | |
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory | |
* A config factory for retrieving required config objects. | |
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer | |
* The CSS asset collection renderer. | |
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer | |
* The JS asset collection renderer. | |
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack | |
* 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, 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; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function processAttachments(AttachmentsInterface $response) { | |
// @todo Convert to assertion once https://www.drupal.org/node/2408013 lands | |
if (!$response instanceof HtmlResponse) { | |
throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.'); | |
} | |
// First, render the actual placeholders; this may cause additional | |
// attachments to be added to the response, which the attachment | |
// placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will | |
// need to include. | |
// | |
// @todo Exceptions should not be used for code flow control. However, the | |
// Form API does not integrate with the HTTP Kernel based architecture of | |
// Drupal 8. In order to resolve this issue properly it is necessary to | |
// completely separate form submission from rendering. | |
// @see https://www.drupal.org/node/2367555 | |
try { | |
$response = $this->renderPlaceholders($response); | |
} | |
catch (EnforcedResponseException $e) { | |
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))); | |
} | |
// 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']); | |
$assets = AttachedAssets::createFromRenderArray(['#attached' => $attached]); | |
// Take Ajax page state into account, to allow for something like | |
// Turbolinks to be implemented without altering core. | |
// @see https://github.com/rails/turbolinks/ | |
$ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state'); | |
$assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); | |
$variables = $this->processAssetLibraries($assets, $attachment_placeholders); | |
// $variables now contains the markup to load the asset libraries. Update | |
// $attached with the final list of libraries and JavaScript settings, so | |
// that $response can be updated with those. Then the response object will | |
// list the final, processed attachments. | |
$attached['library'] = $assets->getLibraries(); | |
$attached['drupalSettings'] = $assets->getSettings(); | |
// 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']) | |
); | |
unset($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']) | |
); | |
unset($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']); | |
} | |
// AttachmentsResponseProcessorInterface mandates that the response it | |
// processes contains the final attachment values. | |
$response->setAttachments($attached); | |
return $response; | |
} | |
/** | |
* Renders placeholders (#attached['placeholders']). | |
* | |
* First, the HTML response object is converted to an equivalent render array, | |
* with #markup being set to the response's content and #attached being set to | |
* the response's attachments. Among these attachments, there may be | |
* placeholders that need to be rendered (replaced). | |
* | |
* Next, RendererInterface::renderRoot() is called, which renders the | |
* placeholders into their final markup. | |
* | |
* The markup that results from RendererInterface::renderRoot() is now the | |
* original HTML response's content, but with the placeholders rendered. We | |
* overwrite the existing content in the original HTML response object with | |
* this markup. The markup that was rendered for the placeholders may also | |
* have attachments (e.g. for CSS/JS assets) itself, and cacheability metadata | |
* that indicates what that markup depends on. That metadata is also added to | |
* the HTML response object. | |
* | |
* @param \Drupal\Core\Render\HtmlResponse $response | |
* The HTML response whose placeholders are being replaced. | |
* | |
* @return \Drupal\Core\Render\HtmlResponse | |
* The updated HTML response, with replaced placeholders. | |
* | |
* @see \Drupal\Core\Render\Renderer::replacePlaceholders() | |
* @see \Drupal\Core\Render\Renderer::renderPlaceholder() | |
*/ | |
protected function renderPlaceholders(HtmlResponse $response) { | |
$build = [ | |
'#markup' => Markup::create($response->getContent()), | |
'#attached' => $response->getAttachments(), | |
]; | |
// RendererInterface::renderRoot() renders the $build render array and | |
// updates it in place. We don't care about the return value (which is just | |
// $build['#markup']), but about the resulting render array. | |
// @todo Simplify this when https://www.drupal.org/node/2495001 lands. | |
$this->renderer->renderRoot($build); | |
// Update the Response object now that the placeholders have been rendered. | |
$placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build); | |
$response | |
->setContent($build['#markup']) | |
->addCacheableDependency($placeholders_bubbleable_metadata) | |
->setAttachments($placeholders_bubbleable_metadata->getAttachments()); | |
return $response; | |
} | |
/** | |
* Processes asset libraries into render arrays. | |
* | |
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets | |
* The attached assets collection for the current response. | |
* @param array $placeholders | |
* The placeholders that exist in the response. | |
* | |
* @return array | |
* An array keyed by asset type, with keys: | |
* - styles | |
* - scripts | |
* - scripts_bottom | |
*/ | |
protected function processAssetLibraries(AttachedAssetsInterface $assets, array $placeholders) { | |
$variables = []; | |
// Print styles - if present. | |
if (isset($placeholders['styles'])) { | |
// Optimize CSS if necessary, but only during normal site operation. | |
$optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess'); | |
$variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css)); | |
} | |
// Print scripts - if any are present. | |
if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) { | |
// Optimize JS if necessary, but only during normal site operation. | |
$optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess'); | |
list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js); | |
$variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header); | |
$variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer); | |
} | |
return $variables; | |
} | |
/** | |
* 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 | |
* An array of placeholders, keyed by type with the placeholders | |
* present in the content of the response as values. | |
* @param array $variables | |
* The variables to render and replace, keyed by type with renderable | |
* arrays as values. | |
*/ | |
protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $response, array $placeholders, array $variables) { | |
$content = $response->getContent(); | |
foreach ($placeholders as $type => $placeholder) { | |
if (isset($variables[$type])) { | |
$content = str_replace($placeholder, $this->renderer->renderPlain($variables[$type]), $content); | |
} | |
} | |
$response->setContent($content); | |
} | |
/** | |
* Sets headers on a response object. | |
* | |
* @param \Drupal\Core\Render\HtmlResponse $response | |
* The HTML response to update. | |
* @param array $headers | |
* 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) { | |
$name = $values[0]; | |
$value = $values[1]; | |
$replace = !empty($values[2]); | |
// Drupal treats the HTTP response status code like a header, even though | |
// it really is not. | |
if (strtolower($name) === 'status') { | |
$response->setStatusCode($value); | |
} | |
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]; | |
} | |
} |