/** * @covers ::preRenderConditionalComments * @dataProvider providerPreRenderConditionalComments */ public function testPreRenderConditionalComments($element, $expected, $set_safe = FALSE) { if ($set_safe) { $element['#prefix'] = SafeString::create($element['#prefix']); $element['#suffix'] = SafeString::create($element['#suffix']); } $this->assertEquals($expected, HtmlTag::preRenderConditionalComments($element)); }
/** * Test that the token replacement in views works correctly. */ public function testViewsTokenReplace() { $text = '{{ langcode__value }} means {{ langcode }}'; $tokens = ['{{ langcode }}' => SafeString::create('English'), '{{ langcode__value }}' => 'en']; $result = \Drupal::service('renderer')->executeInRenderContext(new RenderContext(), function () use($text, $tokens) { return $this->testPluginBase->viewsTokenReplace($text, $tokens); }); $this->assertIdentical($result, 'en means English'); }
/** * Tests that #cache_properties are properly handled. * * @param array $expected_results * An associative array of expected results keyed by property name. * * @covers ::render * @covers ::doRender * @covers \Drupal\Core\Render\RenderCache::get * @covers \Drupal\Core\Render\RenderCache::set * @covers \Drupal\Core\Render\RenderCache::createCacheID * @covers \Drupal\Core\Render\RenderCache::getCacheableRenderArray * * @dataProvider providerTestRenderCacheProperties */ public function testRenderCacheProperties(array $expected_results) { $this->setUpRequest(); $this->setupMemoryCache(); $element = $original = ['#cache' => ['keys' => ['render_cache_test']], '#cache_properties' => array_keys(array_filter($expected_results)), 'child1' => ['#markup' => SafeString::create('1')], 'child2' => ['#markup' => SafeString::create('2')], '#custom_property' => SafeMarkup::checkPlain('custom_value'), '#custom_property_array' => ['custom value']]; $this->renderer->renderRoot($element); $cache = $this->cacheFactory->get('render'); $data = $cache->get('render_cache_test:en:stark')->data; // Check that parent markup is ignored when caching children's markup. $this->assertEquals($data['#markup'] === '', (bool) Element::children($data)); // Check that the element properties are cached as specified. foreach ($expected_results as $property => $expected) { $cached = !empty($data[$property]); $this->assertEquals($cached, (bool) $expected); // Check that only the #markup key is preserved for children. if ($cached) { $this->assertEquals($data[$property], $original[$property]); } } // #custom_property_array can not be a safe_cache_property. $safe_cache_properties = array_diff(Element::properties(array_filter($expected_results)), ['#custom_property_array']); foreach ($safe_cache_properties as $cache_property) { $this->assertTrue(SafeMarkup::isSafe($data[$cache_property]), "{$cache_property} is marked as a safe string"); } }
/** * Applies a very permissive XSS/HTML filter for admin-only use. * * 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 * A string. * * @return \Drupal\Core\Render\SafeString * The escaped string wrapped in a SafeString 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); }
/** * Tests the link method with html. * * @see \Drupal\Core\Utility\LinkGenerator::generate() */ public function testGenerateWithHtml() { $this->urlGenerator->expects($this->at(0))->method('generateFromRoute')->with('test_route_5', array(), $this->defaultOptions)->will($this->returnValue('/test-route-5')); $this->urlGenerator->expects($this->at(1))->method('generateFromRoute')->with('test_route_5', array(), $this->defaultOptions)->will($this->returnValue('/test-route-5')); // Test that HTML tags are stripped from the 'title' attribute. $url = new Url('test_route_5', array(), array('attributes' => array('title' => '<em>HTML Tooltip</em>'))); $url->setUrlGenerator($this->urlGenerator); $result = $this->linkGenerator->generate('Test', $url); $this->assertLink(array('attributes' => array('href' => '/test-route-5', 'title' => 'HTML Tooltip')), $result); // Test that safe HTML is output inside the anchor tag unescaped. The // SafeMarkup::set() call is an intentional unit test for the interaction // between SafeMarkup and the LinkGenerator. $url = new Url('test_route_5', array()); $url->setUrlGenerator($this->urlGenerator); $result = $this->linkGenerator->generate(SafeString::create('<em>HTML output</em>'), $url); $this->assertLink(array('attributes' => array('href' => '/test-route-5'), 'child' => array('tag' => 'em')), $result); $this->assertTrue(strpos($result, '<em>HTML output</em>') !== FALSE); }
/** * Renders a twig string directly. * * Warning: You should use the render element 'inline_template' together with * the #template attribute instead of this method directly. * On top of that you have to ensure that the template string is not dynamic * but just an ordinary static php string, because there may be installations * using read-only PHPStorage that want to generate all possible twig * templates as part of a build step. So it is important that an automated * script can find the templates and extract them. This is only possible if * the template is a regular string. * * @param string $template_string * The template string to render with placeholders. * @param array $context * An array of parameters to pass to the template. * * @return \Drupal\Component\Utility\SafeStringInterface|string * The rendered inline template as a SafeString object. * * @see \Drupal\Core\Template\Loader\StringLoader::exists() */ public function renderInline($template_string, array $context = array()) { // Prefix all inline templates with a special comment. $template_string = '{# inline_template_start #}' . $template_string; return SafeString::create($this->loadTemplate($template_string, NULL)->render($context)); }
/** * Provides the two classes of placeholders: cacheable and uncacheable. * * i.e. with or without #cache[keys]. * * Also, different types: * - A) automatically generated placeholder * - 1) manually triggered (#create_placeholder = TRUE) * - 2) automatically triggered (based on max-age = 0 at the top level) * - 3) automatically triggered (based on high cardinality cache contexts at * the top level) * - 4) automatically triggered (based on high-invalidation frequency cache * tags at the top level) * - 5) automatically triggered (based on max-age = 0 in its subtree, i.e. * via bubbling) * - 6) automatically triggered (based on high cardinality cache contexts in * its subtree, i.e. via bubbling) * - 7) automatically triggered (based on high-invalidation frequency cache * tags in its subtree, i.e. via bubbling) * - B) manually generated placeholder * * So, in total 2*5 = 10 permutations. * * @todo Cases A5, A6 and A7 are not yet supported by core. So that makes for * only 10 permutations currently, instead of 16. That will be done in * https://www.drupal.org/node/2543334 * * @return array */ public function providerPlaceholders() { $args = [$this->randomContextValue()]; $generate_placeholder_markup = function ($cache_keys = NULL) use($args) { $token_render_array = ['#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]; if (is_array($cache_keys)) { $token_render_array['#cache']['keys'] = $cache_keys; } $token = hash('crc32b', serialize($token_render_array)); // \Drupal\Core\Render\SafeString::create() is necessary as the render // system would mangle this markup. As this is exactly what happens at // runtime this is a valid use-case. return SafeString::create('<drupal-render-placeholder callback="Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>'); }; $extract_placeholder_render_array = function ($placeholder_render_array) { return array_intersect_key($placeholder_render_array, ['#lazy_builder' => TRUE, '#cache' => TRUE]); }; // Note the presence of '#create_placeholder'. $base_element_a1 = ['#attached' => ['drupalSettings' => ['foo' => 'bar']], 'placeholder' => ['#cache' => ['contexts' => []], '#create_placeholder' => TRUE, '#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]; // Note the absence of '#create_placeholder', presence of max-age=0 at the // top level. $base_element_a2 = ['#attached' => ['drupalSettings' => ['foo' => 'bar']], 'placeholder' => ['#cache' => ['contexts' => [], 'max-age' => 0], '#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]; // Note the absence of '#create_placeholder', presence of high cardinality // cache context at the top level. $base_element_a3 = ['#attached' => ['drupalSettings' => ['foo' => 'bar']], 'placeholder' => ['#cache' => ['contexts' => ['user']], '#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]; // Note the absence of '#create_placeholder', presence of high-invalidation // frequency cache tag at the top level. $base_element_a4 = ['#attached' => ['drupalSettings' => ['foo' => 'bar']], 'placeholder' => ['#cache' => ['contexts' => [], 'tags' => ['current-temperature']], '#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]; // Note the absence of '#create_placeholder', but the presence of // '#attached[placeholders]'. $base_element_b = ['#markup' => $generate_placeholder_markup(), '#attached' => ['drupalSettings' => ['foo' => 'bar'], 'placeholders' => [(string) $generate_placeholder_markup() => ['#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args]]]]]; $keys = ['placeholder', 'output', 'can', 'be', 'render', 'cached', 'too']; $cases = []; // Case one: render array that has a placeholder that is: // - automatically created, but manually triggered (#create_placeholder = TRUE) // - uncacheable $element_without_cache_keys = $base_element_a1; $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a1['placeholder']); $cases[] = [$element_without_cache_keys, $args, $expected_placeholder_render_array, FALSE, []]; // Case two: render array that has a placeholder that is: // - automatically created, but manually triggered (#create_placeholder = TRUE) // - cacheable $element_with_cache_keys = $base_element_a1; $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys; $expected_placeholder_render_array['#cache']['keys'] = $keys; $cases[] = [$element_with_cache_keys, $args, $expected_placeholder_render_array, $keys, ['#markup' => '<p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['dynamic_animal' => $args[0]]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]]]; // Case three: render array that has a placeholder that is: // - automatically created, and automatically triggered due to max-age=0 // - uncacheable $element_without_cache_keys = $base_element_a2; $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a2['placeholder']); $cases[] = [$element_without_cache_keys, $args, $expected_placeholder_render_array, FALSE, []]; // Case four: render array that has a placeholder that is: // - automatically created, but automatically triggered due to max-age=0 // - cacheable $element_with_cache_keys = $base_element_a2; $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys; $expected_placeholder_render_array['#cache']['keys'] = $keys; $cases[] = [$element_with_cache_keys, $args, $expected_placeholder_render_array, FALSE, []]; // Case five: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // cardinality cache contexts // - uncacheable $element_without_cache_keys = $base_element_a3; $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a3['placeholder']); $cases[] = [$element_without_cache_keys, $args, $expected_placeholder_render_array, FALSE, []]; // Case six: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // cardinality cache contexts // - cacheable $element_with_cache_keys = $base_element_a3; $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys; $expected_placeholder_render_array['#cache']['keys'] = $keys; // The CID parts here consist of the cache keys plus the 'user' cache // context, which in this unit test is simply the given cache context token, // see \Drupal\Tests\Core\Render\RendererTestBase::setUp(). $cid_parts = array_merge($keys, ['user']); $cases[] = [$element_with_cache_keys, $args, $expected_placeholder_render_array, $cid_parts, ['#markup' => '<p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['dynamic_animal' => $args[0]]], '#cache' => ['contexts' => ['user'], 'tags' => [], 'max-age' => Cache::PERMANENT]]]; // Case seven: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // invalidation frequency cache tags // - uncacheable $element_without_cache_keys = $base_element_a4; $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a4['placeholder']); $cases[] = [$element_without_cache_keys, $args, $expected_placeholder_render_array, FALSE, []]; // Case eight: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // invalidation frequency cache tags // - cacheable $element_with_cache_keys = $base_element_a4; $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys; $expected_placeholder_render_array['#cache']['keys'] = $keys; $cases[] = [$element_with_cache_keys, $args, $expected_placeholder_render_array, $keys, ['#markup' => '<p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['dynamic_animal' => $args[0]]], '#cache' => ['contexts' => [], 'tags' => ['current-temperature'], 'max-age' => Cache::PERMANENT]]]; // Case nine: render array that has a placeholder that is: // - manually created // - uncacheable $x = $base_element_b; $expected_placeholder_render_array = $x['#attached']['placeholders'][(string) $generate_placeholder_markup()]; unset($x['#attached']['placeholders'][(string) $generate_placeholder_markup()]['#cache']); $cases[] = [$x, $args, $expected_placeholder_render_array, FALSE, []]; // Case ten: render array that has a placeholder that is: // - manually created // - cacheable $x = $base_element_b; $x['#markup'] = $placeholder_markup = $generate_placeholder_markup($keys); $placeholder_markup = (string) $placeholder_markup; $x['#attached']['placeholders'] = [$placeholder_markup => ['#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback', $args], '#cache' => ['keys' => $keys]]]; $expected_placeholder_render_array = $x['#attached']['placeholders'][$placeholder_markup]; $cases[] = [$x, $args, $expected_placeholder_render_array, $keys, ['#markup' => '<p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['dynamic_animal' => $args[0]]], '#cache' => ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]]]; return $cases; }
/** * {@inheritdoc} */ public function render($hook, array $variables) { static $default_attributes; $active_theme = $this->getActiveTheme(); // If called before all modules are loaded, we do not necessarily have a full // theme registry to work with, and therefore cannot process the theme // request properly. See also \Drupal\Core\Theme\Registry::get(). if (!$this->moduleHandler->isLoaded() && !defined('MAINTENANCE_MODE')) { throw new \Exception(t('_theme() may not be called until all modules are loaded.')); } $theme_registry = $this->themeRegistry->getRuntime(); // If an array of hook candidates were passed, use the first one that has an // implementation. if (is_array($hook)) { foreach ($hook as $candidate) { if ($theme_registry->has($candidate)) { break; } } $hook = $candidate; } // Save the original theme hook, so it can be supplied to theme variable // preprocess callbacks. $original_hook = $hook; // If there's no implementation, check for more generic fallbacks. // If there's still no implementation, log an error and return an empty // string. if (!$theme_registry->has($hook)) { // Iteratively strip everything after the last '__' delimiter, until an // implementation is found. while ($pos = strrpos($hook, '__')) { $hook = substr($hook, 0, $pos); if ($theme_registry->has($hook)) { break; } } if (!$theme_registry->has($hook)) { // Only log a message when not trying theme suggestions ($hook being an // array). if (!isset($candidate)) { \Drupal::logger('theme')->warning('Theme hook %hook not found.', array('%hook' => $hook)); } // There is no theme implementation for the hook passed. Return FALSE so // the function calling _theme() can differentiate between a hook that // exists and renders an empty string and a hook that is not // implemented. return FALSE; } } $info = $theme_registry->get($hook); // If a renderable array is passed as $variables, then set $variables to // the arguments expected by the theme function. if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { $element = $variables; $variables = array(); if (isset($info['variables'])) { foreach (array_keys($info['variables']) as $name) { if (isset($element["#{$name}"]) || array_key_exists("#{$name}", $element)) { $variables[$name] = $element["#{$name}"]; } } } else { $variables[$info['render element']] = $element; // Give a hint to render engines to prevent infinite recursion. $variables[$info['render element']]['#render_children'] = TRUE; } } // Merge in argument defaults. if (!empty($info['variables'])) { $variables += $info['variables']; } elseif (!empty($info['render element'])) { $variables += array($info['render element'] => array()); } // Supply original caller info. $variables += array('theme_hook_original' => $original_hook); // Set base hook for later use. For example if '#theme' => 'node__article' // is called, we run hook_theme_suggestions_node_alter() rather than // hook_theme_suggestions_node__article_alter(), and also pass in the base // hook as the last parameter to the suggestions alter hooks. if (isset($info['base hook'])) { $base_theme_hook = $info['base hook']; } else { $base_theme_hook = $hook; } // Invoke hook_theme_suggestions_HOOK(). $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables)); // If _theme() was invoked with a direct theme suggestion like // '#theme' => 'node__article', add it to the suggestions array before // invoking suggestion alter hooks. if (isset($info['base hook'])) { $suggestions[] = $hook; } // Invoke hook_theme_suggestions_alter() and // hook_theme_suggestions_HOOK_alter(). $hooks = array('theme_suggestions', 'theme_suggestions_' . $base_theme_hook); $this->moduleHandler->alter($hooks, $suggestions, $variables, $base_theme_hook); $this->alter($hooks, $suggestions, $variables, $base_theme_hook); // Check if each suggestion exists in the theme registry, and if so, // use it instead of the hook that _theme() was called with. For example, a // function may call _theme('node', ...), but a module can add // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(), // enabling a theme to have an alternate template file for article nodes. foreach (array_reverse($suggestions) as $suggestion) { if ($theme_registry->has($suggestion)) { $info = $theme_registry->get($suggestion); break; } } // Include a file if the theme function or variable preprocessor is held // elsewhere. if (!empty($info['includes'])) { foreach ($info['includes'] as $include_file) { include_once $this->root . '/' . $include_file; } } // Invoke the variable preprocessors, if any. if (isset($info['base hook'])) { $base_hook = $info['base hook']; $base_hook_info = $theme_registry->get($base_hook); // Include files required by the base hook, since its variable // preprocessors might reside there. if (!empty($base_hook_info['includes'])) { foreach ($base_hook_info['includes'] as $include_file) { include_once $this->root . '/' . $include_file; } } if (isset($base_hook_info['preprocess functions'])) { // Set a variable for the 'theme_hook_suggestion'. This is used to // maintain backwards compatibility with template engines. $theme_hook_suggestion = $hook; } } if (isset($info['preprocess functions'])) { foreach ($info['preprocess functions'] as $preprocessor_function) { if (function_exists($preprocessor_function)) { $preprocessor_function($variables, $hook, $info); } } // Allow theme preprocess functions to set $variables['#attached'] and // $variables['#cache'] and use them like the corresponding element // properties on render arrays. In Drupal 8, this is the (only) officially // supported method of attaching bubbleable metadata from preprocess // functions. Assets attached here should be associated with the template // that we are preprocessing variables for. $preprocess_bubbleable = []; foreach (['#attached', '#cache'] as $key) { if (isset($variables[$key])) { $preprocess_bubbleable[$key] = $variables[$key]; } } // We do not allow preprocess functions to define cacheable elements. unset($preprocess_bubbleable['#cache']['keys']); if ($preprocess_bubbleable) { // @todo Inject the Renderer in https://www.drupal.org/node/2529438. drupal_render($preprocess_bubbleable); } } // Generate the output using either a function or a template. $output = ''; if (isset($info['function'])) { if (function_exists($info['function'])) { // Theme functions do not render via the theme engine, so the output is // not autoescaped. However, we can only presume that the theme function // has been written correctly and that the markup is safe. $output = SafeString::create($info['function']($variables)); } } else { $render_function = 'twig_render_template'; $extension = '.html.twig'; // The theme engine may use a different extension and a different // renderer. $theme_engine = $active_theme->getEngine(); if (isset($theme_engine)) { if ($info['type'] != 'module') { if (function_exists($theme_engine . '_render_template')) { $render_function = $theme_engine . '_render_template'; } $extension_function = $theme_engine . '_extension'; if (function_exists($extension_function)) { $extension = $extension_function(); } } } // In some cases, a template implementation may not have had // template_preprocess() run (for example, if the default implementation // is a function, but a template overrides that default implementation). // In these cases, a template should still be able to expect to have // access to the variables provided by template_preprocess(), so we add // them here if they don't already exist. We don't want the overhead of // running template_preprocess() twice, so we use the 'directory' variable // to determine if it has already run, which while not completely // intuitive, is reasonably safe, and allows us to save on the overhead of // adding some new variable to track that. if (!isset($variables['directory'])) { $default_template_variables = array(); template_preprocess($default_template_variables, $hook, $info); $variables += $default_template_variables; } if (!isset($default_attributes)) { $default_attributes = new Attribute(); } foreach (array('attributes', 'title_attributes', 'content_attributes') as $key) { if (isset($variables[$key]) && !$variables[$key] instanceof Attribute) { if ($variables[$key]) { $variables[$key] = new Attribute($variables[$key]); } else { // Create empty attributes. $variables[$key] = clone $default_attributes; } } } // Render the output using the template file. $template_file = $info['template'] . $extension; if (isset($info['path'])) { $template_file = $info['path'] . '/' . $template_file; } // Add the theme suggestions to the variables array just before rendering // the template for backwards compatibility with template engines. $variables['theme_hook_suggestions'] = $suggestions; // For backwards compatibility, pass 'theme_hook_suggestion' on to the // template engine. This is only set when calling a direct suggestion like // '#theme' => 'menu__shortcut_default' when the template exists in the // current theme. if (isset($theme_hook_suggestion)) { $variables['theme_hook_suggestion'] = $theme_hook_suggestion; } $output = $render_function($template_file, $variables); } return $output instanceof SafeStringInterface ? $output : (string) $output; }
/** * Render this field as user-defined altered text. */ protected function renderAltered($alter, $tokens) { return SafeString::create($this->viewsTokenReplace($alter['text'], $tokens)); }
/** * Tests setting messages and removing one before it is displayed. * * @return string * Empty string, we just test the setting of messages. */ public function drupalSetMessageTest() { // Set two messages. drupal_set_message('First message (removed).'); drupal_set_message(t('Second message with <em>markup!</em> (not removed).')); // Remove the first. unset($_SESSION['messages']['status'][0]); // Duplicate message check. drupal_set_message('Non Duplicated message', 'status', FALSE); drupal_set_message('Non Duplicated message', 'status', FALSE); drupal_set_message('Duplicated message', 'status', TRUE); drupal_set_message('Duplicated message', 'status', TRUE); // Add a SafeString message. drupal_set_message(SafeString::create('SafeString with <em>markup!</em>')); // Test duplicate SafeString messages. drupal_set_message(SafeString::create('SafeString with <em>markup!</em>')); // Ensure that multiple SafeString messages work. drupal_set_message(SafeString::create('SafeString2 with <em>markup!</em>')); // Test mixing of types. drupal_set_message(SafeString::create('Non duplicate SafeString / string.')); drupal_set_message('Non duplicate SafeString / string.'); drupal_set_message(SafeString::create('Duplicate SafeString / string.'), 'status', TRUE); drupal_set_message('Duplicate SafeString / string.', 'status', TRUE); // Test auto-escape of non safe strings. drupal_set_message('<em>This<span>markup will be</span> escaped</em>.'); return []; }
/** * Escapes #plain_text or filters #markup as required. * * Drupal uses Twig's auto-escape feature to improve security. This feature * automatically escapes any HTML that is not known to be safe. Due to this * the render system needs to ensure that all markup it generates is marked * safe so that Twig does not do any additional escaping. * * By default all #markup is filtered to protect against XSS using the admin * tag list. Render arrays can alter the list of tags allowed by the filter * using the #allowed_tags property. This value should be an array of tags * that Xss::filter() would accept. Render arrays can escape text instead * of XSS filtering by setting the #plain_text property instead of #markup. If * #plain_text is used #allowed_tags is ignored. * * @param array $elements * A render array with #markup set. * * @return \Drupal\Component\Utility\SafeStringInterface|string * The escaped markup wrapped in a SafeString object. If * SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be * escaped or filtered again. * * @see \Drupal\Component\Utility\Html::escape() * @see \Drupal\Component\Utility\Xss::filter() * @see \Drupal\Component\Utility\Xss::adminFilter() */ protected function ensureMarkupIsSafe(array $elements) { if (empty($elements['#markup']) && empty($elements['#plain_text'])) { return $elements; } if (!empty($elements['#plain_text'])) { $elements['#markup'] = SafeString::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)); } return $elements; }
/** * Renders all of the fields for a given style and store them on the object. * * @param array $result * The result array from $view->result */ protected function renderFields(array $result) { if (!$this->usesFields()) { return; } if (!isset($this->rendered_fields)) { $this->rendered_fields = []; $this->view->row_index = 0; $field_ids = array_keys($this->view->field); // Only tokens relating to field handlers preceding the one we invoke // ::getRenderTokens() on are returned, so here we need to pick the last // available field handler. $render_tokens_field_id = end($field_ids); // If all fields have a field::access FALSE there might be no fields, so // there is no reason to execute this code. if (!empty($field_ids)) { $renderer = $this->getRenderer(); /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ $cache_plugin = $this->view->display_handler->getPlugin('cache'); /** @var \Drupal\views\ResultRow $row */ foreach ($result as $index => $row) { $this->view->row_index = $index; // Here we implement render caching for result rows. Since we never // build a render array for single rows, given that style templates // need individual field markup to support proper theming, we build // a raw render array containing all field render arrays and cache it. // This allows us to cache the markup of the various children, that is // individual fields, which is then available for style template // preprocess functions, later in the rendering workflow. // @todo Fetch all the available cached row items in one single cache // get operation, once https://www.drupal.org/node/2453945 is fixed. $data = ['#pre_render' => [[$this, 'elementPreRenderRow']], '#row' => $row, '#cache' => ['keys' => $cache_plugin->getRowCacheKeys($row), 'tags' => $cache_plugin->getRowCacheTags($row)], '#cache_properties' => $field_ids]; $renderer->addCacheableDependency($data, $this->view->storage); $renderer->renderPlain($data); // Extract field output from the render array and post process it. $fields = $this->view->field; $rendered_fields =& $this->rendered_fields[$index]; $post_render_tokens = []; foreach ($field_ids as $id) { $rendered_fields[$id] = $data[$id]['#markup']; $tokens = $fields[$id]->postRender($row, $rendered_fields[$id]); if ($tokens) { $post_render_tokens += $tokens; } } // Populate row tokens. $this->rowTokens[$index] = $this->view->field[$render_tokens_field_id]->getRenderTokens([]); // Replace post-render tokens. if ($post_render_tokens) { $placeholders = array_keys($post_render_tokens); $values = array_values($post_render_tokens); foreach ($this->rendered_fields[$index] as &$rendered_field) { // Placeholders and rendered fields have been processed by the // render system and are therefore safe. $rendered_field = SafeString::create(str_replace($placeholders, $values, $rendered_field)); } } } } unset($this->view->row_index); } }
/** * Pre-render callback: Renders #browsers into #prefix and #suffix. * * @param array $element * A render array with a '#browsers' property. The '#browsers' property can * contain any or all of the following keys: * - 'IE': If FALSE, the element is not rendered by Internet Explorer. If * TRUE, the element is rendered by Internet Explorer. Can also be a string * containing an expression for Internet Explorer to evaluate as part of a * conditional comment. For example, this can be set to 'lt IE 7' for the * element to be rendered in Internet Explorer 6, but not in Internet * Explorer 7 or higher. Defaults to TRUE. * - '!IE': If FALSE, the element is not rendered by browsers other than * Internet Explorer. If TRUE, the element is rendered by those browsers. * Defaults to TRUE. * Examples: * - To render an element in all browsers, '#browsers' can be left out or set * to array('IE' => TRUE, '!IE' => TRUE). * - To render an element in Internet Explorer only, '#browsers' can be set * to array('!IE' => FALSE). * - To render an element in Internet Explorer 6 only, '#browsers' can be set * to array('IE' => 'lt IE 7', '!IE' => FALSE). * - To render an element in Internet Explorer 8 and higher and in all other * browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). * * @return array * The passed-in element with markup for conditional comments potentially * added to '#prefix' and '#suffix'. */ public static function preRenderConditionalComments($element) { $browsers = isset($element['#browsers']) ? $element['#browsers'] : array(); $browsers += array('IE' => TRUE, '!IE' => TRUE); // If rendering in all browsers, no need for conditional comments. if ($browsers['IE'] === TRUE && $browsers['!IE']) { return $element; } // Determine the conditional comment expression for Internet Explorer to // evaluate. if ($browsers['IE'] === TRUE) { $expression = 'IE'; } elseif ($browsers['IE'] === FALSE) { $expression = '!IE'; } else { // The IE expression might contain some user input data. $expression = Xss::filterAdmin($browsers['IE']); } // If the #prefix and #suffix properties are used, wrap them with // conditional comment markup. The conditional comment expression is // evaluated by Internet Explorer only. To control the rendering by other // browsers, use either the "downlevel-hidden" or "downlevel-revealed" // technique. See http://en.wikipedia.org/wiki/Conditional_comment // for details. // Ensure what we are dealing with is safe. // This would be done later anyway in drupal_render(). $prefix = isset($element['#prefix']) ? $element['#prefix'] : ''; if ($prefix && !SafeMarkup::isSafe($prefix)) { $prefix = Xss::filterAdmin($prefix); } $suffix = isset($element['#suffix']) ? $element['#suffix'] : ''; if ($suffix && !SafeMarkup::isSafe($suffix)) { $suffix = Xss::filterAdmin($suffix); } // We ensured above that $expression is either a string we created or is // admin XSS filtered, and that $prefix and $suffix are also admin XSS // 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"); } else { // "downlevel-revealed". $element['#prefix'] = SafeString::create("\n<!--[if {$expression}]><!-->\n" . $prefix); $element['#suffix'] = SafeString::create($suffix . "<!--<![endif]-->\n"); } return $element; }
/** * 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' => SafeString::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; }
/** * {@inheritdoc} */ public function getCacheableRenderArray(array $elements) { $data = ['#markup' => $elements['#markup'], '#attached' => $elements['#attached'], '#cache' => ['contexts' => $elements['#cache']['contexts'], 'tags' => $elements['#cache']['tags'], 'max-age' => $elements['#cache']['max-age']]]; // Preserve cacheable items if specified. If we are preserving any cacheable // children of the element, we assume we are only interested in their // individual markup and not the parent's one, thus we empty it to minimize // 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. 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]); } } // Extract all the cacheable items from the element using cache // properties. $cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties'])); $cacheable_children = Element::children($cacheable_items); if ($cacheable_children) { $data['#markup'] = ''; // 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'])]; } } $data += $cacheable_items; } $data['#markup'] = SafeString::create($data['#markup']); return $data; }