Commit 8c684918 authored by webchick's avatar webchick

Issue #2273277 by Wim Leers, effulgentsia, Fabianx: Fixed Figure out a...

Issue #2273277 by Wim Leers, effulgentsia, Fabianx: Fixed Figure out a solution for the problematic interaction between the render system and the theme system when using #pre_render.
parent dcc880bc
This diff is collapsed.
......@@ -11,6 +11,7 @@
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\StorageException;
......@@ -1750,6 +1751,14 @@ function template_preprocess_html(&$variables) {
$page->addMetaElement($metatag);
}
}
// Add favicon.
if (theme_get_setting('features.favicon')) {
$url = UrlHelper::stripDangerousProtocols(theme_get_setting('favicon.url'));
$link = new LinkElement($url, 'shortcut icon', ['type' => theme_get_setting('favicon.mimetype')]);
$page->addLinkElement($link);
}
$variables['page_top'][] = array('#markup' => $page->getBodyTop());
$variables['page_bottom'][] = array('#markup' => $page->getBodyBottom());
}
......
......@@ -78,7 +78,9 @@ public function render($content) {
* @todo: Remove as part of https://drupal.org/node/2182149
*/
protected function drupalRender(&$elements, $is_recursive_call = FALSE) {
return drupal_render($elements, $is_recursive_call);
$output = drupal_render($elements, $is_recursive_call);
drupal_process_attached($elements);
return $output;
}
/**
......
......@@ -150,7 +150,7 @@ public function render() {
*/
protected function drupalAttachLibrary($name) {
$attached['#attached']['library'][] = $name;
drupal_render($attached);
drupal_process_attached($attached);
}
}
......@@ -93,6 +93,7 @@ public function dialog(Request $request, $_content, $modal = FALSE) {
}
$content = drupal_render($page_content);
drupal_process_attached($page_content);
$title = isset($page_content['#title']) ? $page_content['#title'] : $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
$response = new AjaxResponse();
// Fetch any modal options passed in from data-dialog-options.
......
......@@ -73,6 +73,9 @@ protected function createHtmlFragment($page_content, Request $request) {
}
$content = $this->drupalRender($page_content);
if (!empty($page_content)) {
drupal_process_attached($page_content);
}
$cache = !empty($page_content['#cache']['tags']) ? array('tags' => $page_content['#cache']['tags']) : array();
$fragment = new HtmlFragment($content, $cache);
......@@ -85,7 +88,7 @@ protected function createHtmlFragment($page_content, Request $request) {
}
// Add feed links from the page content.
$attached = drupal_render_collect_attached($page_content, TRUE);
$attached = isset($page_content['#attached']) ? $page_content['#attached'] : array();
if (!empty($attached['drupal_add_feed'])) {
foreach ($attached['drupal_add_feed'] as $feed) {
$feed_link = new FeedLinkElement($feed[1], $this->urlGenerator->generateFromPath($feed[0]));
......
......@@ -59,6 +59,14 @@ public function render(HtmlFragmentInterface $fragment, $status_code = 200) {
$page->setContent(drupal_render($page_array));
$page->setStatusCode($status_code);
drupal_process_attached($page_array);
if (isset($page_array['page_top'])) {
drupal_process_attached($page_array['page_top']);
}
if (isset($page_array['page_bottom'])) {
drupal_process_attached($page_array['page_bottom']);
}
if ($fragment instanceof CacheableInterface) {
// Collect cache tags for all the content in all the regions on the page.
$tags = $page_array['#cache']['tags'];
......@@ -102,6 +110,21 @@ public function preparePage(HtmlPage $page, &$page_array) {
$page->addLinkElement($link);
}
// Add libraries and CSS used by this theme.
$active_theme = \Drupal::theme()->getActiveTheme();
foreach ($active_theme->getLibraries() as $library) {
$page_array['#attached']['library'][] = $library;
}
foreach ($active_theme->getStyleSheets() as $media => $stylesheets) {
foreach ($stylesheets as $stylesheet) {
$page_array['#attached']['css'][$stylesheet] = array(
'group' => CSS_AGGREGATE_THEME,
'every_page' => TRUE,
'media' => $media
);
}
}
return $page;
}
......
......@@ -20,6 +20,15 @@ public function render(HtmlPage $page) {
'#type' => 'html',
'#page_object' => $page,
);
// drupal_render() will render the 'html' template, which will call
// HtmlPage::getScripts(). But normally we can only run
// drupal_process_attached() after drupal_render(). Hence any assets
// attached to '#type' => 'html' will be lost. This is a work-around for
// that limitation, until the HtmlPage object contains its assets — this is
// an unfortunate intermediate consequence of the way HtmlPage dictates page
// rendering and how that differs from how drupal_render() works.
$render += element_info($render['#type']);
drupal_process_attached($render);
return drupal_render($render);
}
......@@ -102,6 +111,13 @@ public static function renderPage($main, $title = '', $theme = 'maintenance', ar
$page->setBodyTop(drupal_render($page_array['page_top']));
$page->setBodyBottom(drupal_render($page_array['page_bottom']));
$page->setContent(drupal_render($page_array));
drupal_process_attached($page_array);
if (isset($page_array['page_top'])) {
drupal_process_attached($page_array['page_top']);
}
if (isset($page_array['page_bottom'])) {
drupal_process_attached($page_array['page_bottom']);
}
return \Drupal::service('html_page_renderer')->render($page);
}
......
......@@ -7,8 +7,6 @@
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\UrlHelper;
/**
* Provides a render element for <html>.
*
......@@ -20,12 +18,8 @@ class Html extends RenderElement {
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#theme' => 'html',
'#pre_render' => array(
array($class, 'preRenderHtml'),
),
// HTML5 Shiv
'#attached' => array(
'library' => array('core/html5shiv'),
......@@ -33,35 +27,4 @@ public function getInfo() {
);
}
/**
* #pre_render callback for the html element type.
*
* @param array $element
* A structured array containing the html element type build properties.
*
* @return array
* The processed element.
*/
public static function preRenderHtml($element) {
// Add favicon.
if (static::themeGetSetting('features.favicon')) {
$favicon = static::themeGetSetting('favicon.url');
$type = static::themeGetSetting('favicon.mimetype');
$element['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'shortcut icon',
'href' => UrlHelper::stripDangerousProtocols($favicon),
'type' => $type,
);
}
return $element;
}
/**
* Wraps theme_get_setting().
*/
protected static function themeGetSetting($setting_name) {
return theme_get_setting($setting_name);
}
}
......@@ -56,7 +56,9 @@ public function getInfo($type) {
if (!isset($this->elementInfo)) {
$this->elementInfo = $this->buildInfo();
}
return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
$info = isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
$info['#defaults_loaded'] = TRUE;
return $info;
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Render\RenderStackFrame.
*/
namespace Drupal\Core\Render;
/**
* Value object used for bubbleable rendering metadata.
*
* @see drupal_render()
*/
class RenderStackFrame {
/**
* Cache tags.
*
* @var array
*/
public $tags = [];
/**
* Attached assets.
*
* @var array
*/
public $attached = [];
/**
* #post_render_cache metadata.
*
* @var array
*/
public $postRenderCache = [];
}
......@@ -741,7 +741,7 @@ function template_preprocess_comment(&$variables) {
}
if (isset($variables['elements']['signature'])) {
$variables['signature'] = $variables['elements']['signature']['#markup'];
$variables['signature'] = $variables['elements']['signature'];
unset($variables['elements']['signature']);
}
else {
......
......@@ -72,7 +72,7 @@ public function renderForm(array $element, array $context) {
$form = $this->entityFormBuilder->getForm($comment);
// @todo: This only works as long as assets are still tracked in a global
// static variable, see https://drupal.org/node/2238835
$markup = drupal_render($form, TRUE);
$markup = drupal_render($form);
$callback = 'comment.post_render_cache:renderForm';
$placeholder = drupal_render_cache_generate_placeholder($callback, $context);
......
......@@ -138,11 +138,6 @@ public function buildComponents(array &$build, array $entities, array $displays,
'#format' => $account->getSignatureFormat(),
'#langcode' => $entity->language()->getId(),
);
// The signature will only be rendered in the theme layer, which means
// its associated cache tags will not bubble up. Work around this for
// now by already rendering the signature here.
// @todo remove this work-around, see https://drupal.org/node/2273277
drupal_render($build[$id]['signature'], TRUE);
}
if (!isset($build[$id]['#attached'])) {
......
......@@ -162,20 +162,6 @@ public function viewElements(FieldItemListInterface $items) {
if ($this->getSetting('pager_id')) {
$build['pager']['#element'] = $this->getSetting('pager_id');
}
// The viewElements() method of entity field formatters is run
// during the #pre_render phase of rendering an entity. A formatter
// builds the content of the field in preparation for theming.
// All entity cache tags must be available after the #pre_render phase.
// This field formatter is highly exceptional: it renders *another*
// entity and this referenced entity has its own #pre_render
// callbacks. In order collect the cache tags associated with the
// referenced entity it must be passed to drupal_render() so that its
// #pre_render callbacks are invoked and its full build array is
// assembled. Rendering the referenced entity in place here will allow
// its cache tags to be bubbled up and included with those of the
// main entity when cache tags are collected for a renderable array
// in drupal_render().
drupal_render($build, TRUE);
$output['comments'] = $build;
}
}
......
......@@ -76,7 +76,8 @@ protected function createEntity() {
* Each comment must have a comment body, which always has a text format.
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) {
return array('filter_format:plain_text');
/** @var \Drupal\comment\CommentInterface $entity */
return array('filter_format:plain_text', 'user:' . $entity->getOwnerId(), 'user_view:1');
}
}
......@@ -67,7 +67,7 @@ public function testCacheTags() {
drupal_render($build);
$expected_cache_tags = array(
'entity_test_view' => TRUE,
'entity_test' => array(1 => $commented_entity->id()),
'entity_test' => array($commented_entity->id()),
);
$this->assertEqual($build['#cache']['tags'], $expected_cache_tags, 'The test entity has the expected cache tags before it has comments.');
......@@ -102,12 +102,14 @@ public function testCacheTags() {
drupal_render($build);
$expected_cache_tags = array(
'entity_test_view' => TRUE,
'entity_test' => array(1 => $commented_entity->id()),
'entity_test' => array($commented_entity->id()),
'comment_view' => TRUE,
'comment' => array(1 => $comment->id()),
'filter_format' => array(
'plain_text' => 'plain_text',
),
'user_view' => TRUE,
'user' => array(2 => 2),
);
$this->assertEqual($build['#cache']['tags'], $expected_cache_tags, 'The test entity has the expected cache tags when it has comments.');
}
......
......@@ -95,23 +95,7 @@ public function viewElements(FieldItemListInterface $items) {
}
if (!empty($item->target_id)) {
// The viewElements() method of entity field formatters is run
// during the #pre_render phase of rendering an entity. A formatter
// builds the content of the field in preparation for theming.
// All entity cache tags must be available after the #pre_render phase.
// This field formatter is highly exceptional: it renders *another*
// entity and this referenced entity has its own #pre_render
// callbacks. In order collect the cache tags associated with the
// referenced entity it must be passed to drupal_render() so that its
// #pre_render callbacks are invoked and its full build array is
// assembled. Rendering the referenced entity in place here will allow
// its cache tags to be bubbled up and included with those of the
// main entity when cache tags are collected for a renderable array
// in drupal_render().
// @todo remove this work-around, see https://drupal.org/node/2273277
$referenced_entity_build = entity_view($item->entity, $view_mode, $item->getLangcode());
drupal_render($referenced_entity_build, TRUE);
$elements[$delta] = $referenced_entity_build;
$elements[$delta] = entity_view($item->entity, $view_mode, $item->getLangcode());
if (empty($links) && isset($result[$delta][$target_type][$item->target_id]['links'])) {
// Hide the element links.
......
......@@ -181,10 +181,11 @@ public function testEntityFormatter() {
</div>
</div>
';
drupal_render($build[0]);
$this->assertEqual($build[0]['#markup'], 'default | ' . $this->referencedEntity->label() . $expected_rendered_name_field . $expected_rendered_body_field, format_string('The markup returned by the @formatter formatter is correct.', array('@formatter' => $formatter)));
$expected_cache_tags = array(
$this->entityType . '_view' => TRUE,
$this->entityType => array($this->referencedEntity->id() => $this->referencedEntity->id()),
$this->entityType => array($this->referencedEntity->id()),
'filter_format' => array('full_html' => 'full_html'),
);
$this->assertEqual($build[0]['#cache']['tags'], $expected_cache_tags, format_string('The @formatter formatter has the expected cache tags.', array('@formatter' => $formatter)));
......
......@@ -118,7 +118,8 @@ function testFieldItemListView() {
$items = $this->entity->get($this->field_name);
// No display settings: check that default display settings are used.
$this->render($items->view());
$build = $items->view();
$this->render($build);
$settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('field_test_default');
$setting = $settings['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
......@@ -135,7 +136,8 @@ function testFieldItemListView() {
'alter' => TRUE,
),
);
$this->render($items->view($display));
$build = $items->view($display);
$this->render($build);
$setting = $display['settings']['test_formatter_setting_multiple'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertText('field_test_entity_display_build_alter', 'Alter fired, display passed.');
......@@ -155,7 +157,8 @@ function testFieldItemListView() {
'alter' => TRUE,
),
);
$this->render($items->view($display));
$build = $items->view($display);
$this->render($build);
$setting = $display['settings']['test_formatter_setting_multiple'];
$this->assertRaw('visually-hidden', 'Label was visually hidden.');
$this->assertText('field_test_entity_display_build_alter', 'Alter fired, display passed.');
......@@ -174,7 +177,8 @@ function testFieldItemListView() {
'test_formatter_setting_additional' => $this->randomMachineName(),
),
);
$this->render($items->view($display));
$build = $items->view($display);
$this->render($build);
$setting = $display['settings']['test_formatter_setting_additional'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertNoText('field_test_entity_display_build_alter', 'Alter not fired.');
......@@ -184,7 +188,8 @@ function testFieldItemListView() {
// View mode: check that display settings specified in the display object
// are used.
$this->render($items->view('teaser'));
$build = $items->view('teaser');
$this->render($build);
$setting = $this->display_options['teaser']['settings']['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
......@@ -193,7 +198,8 @@ function testFieldItemListView() {
// Unknown view mode: check that display settings for 'default' view mode
// are used.
$this->render($items->view('unknown_view_mode'));
$build = $items->view('unknown_view_mode');
$this->render($build);
$setting = $this->display_options['default']['settings']['test_formatter_setting'];
$this->assertText($this->label, 'Label was displayed.');
foreach ($this->values as $delta => $value) {
......@@ -210,7 +216,8 @@ function testFieldItemView() {
$setting = $settings['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[$delta];
$this->render($item->view());
$build = $item->view();
$this->render($build);
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
......@@ -224,7 +231,8 @@ function testFieldItemView() {
$setting = $display['settings']['test_formatter_setting_multiple'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[$delta];
$this->render($item->view($display));
$build = $item->view($display);
$this->render($build);
$this->assertText($setting . '|0:' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
......@@ -238,7 +246,8 @@ function testFieldItemView() {
$setting = $display['settings']['test_formatter_setting_additional'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[$delta];
$this->render($item->view($display));
$build = $item->view($display);
$this->render($build);
$this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
......@@ -247,7 +256,8 @@ function testFieldItemView() {
$setting = $this->display_options['teaser']['settings']['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[$delta];
$this->render($item->view('teaser'));
$build = $item->view('teaser');
$this->render($build);
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
......@@ -256,7 +266,8 @@ function testFieldItemView() {
$setting = $this->display_options['default']['settings']['test_formatter_setting'];
foreach ($this->values as $delta => $value) {
$item = $this->entity->{$this->field_name}[$delta];
$this->render($item->view('unknown_view_mode'));
$build = $item->view('unknown_view_mode');
$this->render($build);
$this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
}
......@@ -275,7 +286,8 @@ function testFieldEmpty() {
);
// $this->entity is set by the setUp() method and by default contains 4
// numeric values. We only want to test the display of this one field.
$this->render($this->entity->get($this->field_name)->view($display));
$build = $this->entity->get($this->field_name)->view($display);
$this->render($build);
// The test field by default contains values, so should not display the
// default "empty" text.
$this->assertNoText($display['settings']['test_empty_string']);
......@@ -283,7 +295,8 @@ function testFieldEmpty() {
// Now remove the values from the test field and retest.
$this->entity->{$this->field_name} = array();
$this->entity->save();
$this->render($this->entity->get($this->field_name)->view($display));
$build = $this->entity->get($this->field_name)->view($display);
$this->render($build);
// This time, as the field values have been removed, we *should* show the
// default "empty" text.
$this->assertText($display['settings']['test_empty_string']);
......
......@@ -75,6 +75,7 @@ public function upload(Request $request) {
$status_messages = array('#theme' => 'status_messages');
$form['#prefix'] .= drupal_render($status_messages);
$output = drupal_render($form);
drupal_process_attached($form);
$js = _drupal_add_js();
$settings = drupal_merge_js_settings($js['settings']['data']);
......
......@@ -247,12 +247,12 @@ function testProcessedTextElement() {
$expected_cache_tags = array(
// The cache tag set by the processed_text element itself.
'filter_format' => array(
'element_test' => 'element_test',
'element_test',
),
// The cache tags set by the filter_test_cache_tags filter.
'foo' => array(
'bar' => 'bar',
'baz' => 'baz',
'bar',
'baz',
),
);
$this->assertEqual($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.');
......
......@@ -32,6 +32,7 @@ class LocaleLibraryInfoAlterTest extends WebTestBase {
public function testLibraryInfoAlter() {
$attached['#attached']['library'][] = 'core/jquery.ui.datepicker';
drupal_render($attached);
drupal_process_attached($attached);
$scripts = drupal_get_js();
$this->assertTrue(strpos($scripts, 'locale.datepicker.js'), 'locale.datepicker.js added to scripts.');
}
......
......@@ -60,7 +60,7 @@ protected function createEntity() {
* Each node must have an author.
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $node) {
return array('user:' . $node->getOwnerId());
return array('user:' . $node->getOwnerId(), 'user_view:1');
}
}
......@@ -547,8 +547,9 @@ protected function unregisterStreamWrapper($scheme, $type) {
* @return string
* The rendered string output (typically HTML).
*/
protected function render(array $elements) {
protected function render(array &$elements) {
$content = drupal_render($elements);
drupal_process_attached($elements);
$this->setRawContent($content);
$this->verbose('<pre style="white-space: pre-wrap">' . String::checkPlain($content));
return $content;
......
......@@ -18,6 +18,7 @@
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Session\UserSession;
......@@ -341,26 +342,37 @@ protected function drupalCreateContentType(array $values = array()) {
* @see drupal_render()
*/
protected function drupalBuildEntityView(EntityInterface $entity, $view_mode = 'full', $langcode = NULL, $reset = FALSE) {
$ensure_fully_built = function(&$elements) use (&$ensure_fully_built) {
// If the default values for this element have not been loaded yet, populate
// them.
if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
$elements += element_info($elements['#type']);
}
// Make any final changes to the element before it is rendered. This means
// that the $element or the children can be altered or corrected before the
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
$elements = call_user_func($callable, $elements);
}
}
// And recurse.
$children = Element::children($elements, TRUE);
foreach ($children as $key) {
$ensure_fully_built($elements[$key]);
}
};
$render_controller = $this->container->get('entity.manager')->getViewBuilder($entity->getEntityTypeId());
if ($reset) {
$render_controller->resetCache(array($entity->id()));
}
$elements = $render_controller->view($entity, $view_mode, $langcode);
// If the default values for this element have not been loaded yet, populate
// them.
if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
$elements += element_info($elements['#type']);
}
$build = $render_controller->view($entity, $view_mode, $langcode);
$ensure_fully_built($build);
// Make any final changes to the element before it is rendered. This means
// that the $element or the children can be altered or corrected before the
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
$elements = call_user_func($callable, $elements);
}
}
return $elements;
return $build;
}
/**
......
......@@ -110,6 +110,14 @@ public function render(array $output, $status_code = 200) {
$page->setBodyBottom(drupal_render($page_array['page_bottom']));
$page->setContent(drupal_render($page_array));
drupal_process_attached($page_array);
if (isset($page_array['page_top'])) {
drupal_process_attached($page_array['page_top']);
}
if (isset($page_array['page_bottom'])) {
drupal_process_attached($page_array['page_bottom']);
}
$page->setStatusCode($status_code);
return $page;
......
......@@ -50,6 +50,7 @@ public function testOrder() {
),
);
drupal_render($attached);
drupal_process_attached($attached);