/** * 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'], ], // Collect expected property names. '#cache_properties' => array_keys(array_filter($expected_results)), 'child1' => ['#markup' => Markup::create('1')], 'child2' => ['#markup' => Markup::create('2')], // Mark the value as safe. '#custom_property' => Markup::create('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"); } }
/** * @covers ::preRenderConditionalComments * @dataProvider providerPreRenderConditionalComments */ public function testPreRenderConditionalComments($element, $expected, $set_safe = FALSE) { if ($set_safe) { $element['#prefix'] = Markup::create($element['#prefix']); $element['#suffix'] = Markup::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 }}' => Markup::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 [entity:field_name] tokens. */ public function testEntityFieldTokens() { // Create a node with a value in the text field and test its token. $format = FilterFormat::create(['format' => 'test', 'weight' => 1, 'filters' => ['filter_html_escape' => ['status' => TRUE]]]); $format->save(); $entity = Node::create(['title' => 'Test node title', 'type' => 'article', 'test_field' => ['value' => 'foo', 'format' => $format->id()]]); $entity->save(); $this->assertTokens('node', ['node' => $entity], ['test_field' => Markup::create('foo')]); // Create a node without a value in the text field and test its token. $entity = Node::create(['title' => 'Test node title', 'type' => 'article']); $entity->save(); $this->assertNoTokens('node', ['node' => $entity], ['test_field']); }
/** * Gets all BigPipe placeholder test cases. * * @param \Symfony\Component\DependencyInjection\ContainerInterface|null $container * Optional. Necessary to get the embedded AJAX/HTML responses. * @param \Drupal\Core\Session\AccountInterface|null $user * Optional. Necessary to get the embedded AJAX/HTML responses. * * @return \Drupal\big_pipe\Tests\BigPipePlaceholderTestCase[] */ public static function cases(ContainerInterface $container = NULL, AccountInterface $user = NULL) { // Define the two types of cacheability that we expect to see. These will be // used in the expectations. $cacheability_depends_on_session_only = ['max-age' => 0, 'contexts' => ['session.exists']]; $cacheability_depends_on_session_and_nojs_cookie = ['max-age' => 0, 'contexts' => ['session.exists', 'cookies:big_pipe_nojs']]; // 1. Real-world example of HTML placeholder. $status_messages = new BigPipePlaceholderTestCase([], '<drupal-render-placeholder callback="Drupal\\Core\\Render\\Element\\StatusMessages::renderMessages" arguments="0" token="a8c34b5e"></drupal-render-placeholder>', ['#lazy_builder' => ['Drupal\\Core\\Render\\Element\\StatusMessages::renderMessages', [NULL]]]); $status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e'; $status_messages->bigPipePlaceholderRenderArray = ['#markup' => '<div data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>', '#cache' => $cacheability_depends_on_session_and_nojs_cookie, '#attached' => ['library' => ['big_pipe/big_pipe'], 'drupalSettings' => ['bigPipePlaceholderIds' => ['callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e' => TRUE]], 'big_pipe_placeholders' => ['callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e' => $status_messages->placeholderRenderArray]]]; $status_messages->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>'; $status_messages->bigPipeNoJsPlaceholderRenderArray = ['#markup' => '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>', '#cache' => $cacheability_depends_on_session_and_nojs_cookie, '#attached' => ['big_pipe_nojs_placeholders' => ['<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"></div>' => $status_messages->placeholderRenderArray]]]; if ($container && $user) { $status_messages->embeddedAjaxResponseCommands = [['command' => 'settings', 'settings' => ['ajaxPageState' => ['theme' => 'classy', 'libraries' => 'big_pipe/big_pipe,classy/base,classy/messages,core/drupal.active-link,core/html5shiv,core/normalize,system/base'], 'pluralDelimiter' => \Drupal\Core\StringTranslation\PluralTranslatableMarkup::DELIMITER, 'user' => ['uid' => '1', 'permissionsHash' => $container->get('user_permissions_hash_generator')->generate($user)]], 'merge' => TRUE], ['command' => 'add_css', 'data' => '<link rel="stylesheet" href="' . base_path() . 'core/themes/classy/css/components/messages.css?' . $container->get('state')->get('system.css_js_query_string') . '" media="all" />' . "\n"], ['command' => 'insert', 'method' => 'replaceWith', 'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e"]', 'data' => "\n" . ' <div role="contentinfo" aria-label="Status message" class="messages messages--status">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n \n", 'settings' => NULL]]; $status_messages->embeddedHtmlResponse = '<link rel="stylesheet" href="' . base_path() . 'core/themes/classy/css/components/messages.css?' . $container->get('state')->get('system.css_js_query_string') . '" media="all" />' . "\n" . "\n" . ' <div role="contentinfo" aria-label="Status message" class="messages messages--status">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n \n"; } // 2. Real-world example of HTML attribute value placeholder: form action. $form_action = new BigPipePlaceholderTestCase($container ? $container->get('form_builder')->getForm('Drupal\\big_pipe_test\\Form\\BigPipeTestForm') : [], 'form_action_cc611e1d', ['#lazy_builder' => ['form_builder:renderPlaceholderFormAction', []]]); $form_action->bigPipeNoJsPlaceholder = 'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d'; $form_action->bigPipeNoJsPlaceholderRenderArray = ['#markup' => 'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d', '#cache' => $cacheability_depends_on_session_only, '#attached' => ['big_pipe_nojs_placeholders' => ['big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d' => $form_action->placeholderRenderArray]]]; if ($container) { $form_action->embeddedHtmlResponse = '<form class="big-pipe-test-form" data-drupal-selector="big-pipe-test-form" action="' . base_path() . 'big_pipe_test"'; } // 3. Real-world example of HTML attribute value subset placeholder: CSRF // token in link. $csrf_token = new BigPipePlaceholderTestCase(['#title' => 'Link with CSRF token', '#type' => 'link', '#url' => Url::fromRoute('system.theme_set_default')], 'e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e', ['#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', ['admin/config/user-interface/shortcut/manage/default/add-link-inline']]]); $csrf_token->bigPipeNoJsPlaceholder = 'big_pipe_nojs_placeholder_attribute_safe:e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e'; $csrf_token->bigPipeNoJsPlaceholderRenderArray = ['#markup' => 'big_pipe_nojs_placeholder_attribute_safe:e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e', '#cache' => $cacheability_depends_on_session_only, '#attached' => ['big_pipe_nojs_placeholders' => ['big_pipe_nojs_placeholder_attribute_safe:e88b559cce72c80b687d56b0e2a3a5ae4b66bc0e' => $csrf_token->placeholderRenderArray]]]; if ($container) { $csrf_token->embeddedHtmlResponse = $container->get('csrf_token')->get('admin/appearance/default'); } // 4. Edge case: custom string to be considered as a placeholder that // happens to not be valid HTML. $hello = new BigPipePlaceholderTestCase(['#markup' => Markup::create('<hello'), '#attached' => ['placeholders' => ['<hello' => ['#lazy_builder' => ['\\Drupal\\big_pipe_test\\BigPipeTestController::helloOrYarhar', []]]]]], '<hello', ['#lazy_builder' => ['hello_or_yarhar', []]]); $hello->bigPipeNoJsPlaceholder = 'big_pipe_nojs_placeholder_attribute_safe:<hello'; $hello->bigPipeNoJsPlaceholderRenderArray = ['#markup' => 'big_pipe_nojs_placeholder_attribute_safe:<hello', '#cache' => $cacheability_depends_on_session_only, '#attached' => ['big_pipe_nojs_placeholders' => ['big_pipe_nojs_placeholder_attribute_safe:<hello' => $hello->placeholderRenderArray]]]; $hello->embeddedHtmlResponse = '<marquee>Yarhar llamas forever!</marquee>'; // 5. Edge case: non-#lazy_builder placeholder. $current_time = new BigPipePlaceholderTestCase(['#markup' => Markup::create('<time>CURRENT TIME</time>'), '#attached' => ['placeholders' => ['<time>CURRENT TIME</time>' => ['#pre_render' => ['\\Drupal\\big_pipe_test\\BigPipeTestController::currentTime']]]]], '<time>CURRENT TIME</time>', ['#pre_render' => ['current_time']]); $current_time->bigPipePlaceholderId = 'timecurrent-timetime'; $current_time->bigPipePlaceholderRenderArray = ['#markup' => '<div data-big-pipe-placeholder-id="timecurrent-timetime"></div>', '#cache' => $cacheability_depends_on_session_and_nojs_cookie, '#attached' => ['library' => ['big_pipe/big_pipe'], 'drupalSettings' => ['bigPipePlaceholderIds' => ['timecurrent-timetime' => TRUE]], 'big_pipe_placeholders' => ['timecurrent-timetime' => $current_time->placeholderRenderArray]]]; $current_time->embeddedAjaxResponseCommands = [['command' => 'insert', 'method' => 'replaceWith', 'selector' => '[data-big-pipe-placeholder-id="timecurrent-timetime"]', 'data' => '<time datetime=1991-03-14"></time>', 'settings' => NULL]]; $current_time->bigPipeNoJsPlaceholder = '<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>'; $current_time->bigPipeNoJsPlaceholderRenderArray = ['#markup' => '<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>', '#cache' => $cacheability_depends_on_session_and_nojs_cookie, '#attached' => ['big_pipe_nojs_placeholders' => ['<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>' => $current_time->placeholderRenderArray]]]; $current_time->embeddedHtmlResponse = '<time datetime=1991-03-14"></time>'; return ['html' => $status_messages, 'html_attribute_value' => $form_action, 'html_attribute_value_subset' => $csrf_token, 'edge_case__invalid_html' => $hello, 'edge_case__html_non_lazy_builder' => $current_time]; }
/** * Sets up the unrouted url assembler and the link generator. */ protected function setUpUrlIntegrationServices() { $this->pathProcessor = $this->getMock('Drupal\\Core\\PathProcessor\\OutboundPathProcessorInterface'); $this->unroutedUrlAssembler = new UnroutedUrlAssembler($this->requestStack, $this->pathProcessor); \Drupal::getContainer()->set('unrouted_url_assembler', $this->unroutedUrlAssembler); $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->getMock('Drupal\\Core\\Extension\\ModuleHandlerInterface'), $this->renderer); $this->renderer->method('render')->willReturnCallback(function (&$elements, $is_root_call = FALSE) { // Mock the ability to theme links $link = $this->linkGenerator->generate($elements['#title'], $elements['#url']); if (isset($elements['#prefix'])) { $link = $elements['#prefix'] . $link; } if (isset($elements['#suffix'])) { $link = $link . $elements['#suffix']; } return Markup::create($link); }); }
/** * Wrapper for \Drupal\Core\Render\Markup::create(). * * @param string $input * The input string to mark as safe. * * @return string * The unaltered input value. */ protected function setSafeMarkup($input) { return Markup::create($input); }
/** * 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'] = Markup::create("\n<!--[if {$expression}]>\n" . $prefix); $element['#suffix'] = Markup::create($suffix . "<![endif]-->\n"); } else { // "downlevel-revealed". $element['#prefix'] = Markup::create("\n<!--[if {$expression}]><!-->\n" . $prefix); $element['#suffix'] = Markup::create($suffix . "<!--<![endif]-->\n"); } return $element; }
/** * Gets a rendered link from a url object. * * @param string $text * The link text for the anchor tag as a translated string. * @param \Drupal\Core\Url|string $url * The URL object or string used for the link. * @param array|\Drupal\Core\Template\Attribute $attributes * An optional array or Attribute object of link attributes. * * @return array * A render array representing a link to the given URL. */ public function getLink($text, $url, $attributes = []) { if (!$url instanceof Url) { $url = Url::fromUri($url); } if ($attributes) { if ($attributes instanceof Attribute) { $attributes = $attributes->toArray(); } if ($existing_attributes = $url->getOption('attributes')) { $attributes = array_merge($existing_attributes, $attributes); } $url->setOption('attributes', $attributes); } // The text has been processed by twig already, convert it to a safe object // for the render system. if ($text instanceof \Twig_Markup) { $text = Markup::create($text); } $build = ['#type' => 'link', '#title' => $text, '#url' => $url]; return $build; }
public function providerTestReplaceEscaping() { $data = []; // No tokens. The first argument to Token::replace() should not be escaped. $data['no-tokens'] = ['muh', [], 'muh']; $data['html-in-string'] = ['<h1>Giraffe</h1>', [], '<h1>Giraffe</h1>']; $data['html-in-string-quote'] = ['<h1>Giraffe"</h1>', [], '<h1>Giraffe"</h1>']; $data['simple-placeholder-with-plain-text'] = ['<h1>[token:meh]</h1>', ['[token:meh]' => 'Giraffe"'], '<h1>' . Html::escape('Giraffe"') . '</h1>']; $data['simple-placeholder-with-safe-html'] = ['<h1>[token:meh]</h1>', ['[token:meh]' => Markup::create('<em>Emphasized</em>')], '<h1><em>Emphasized</em></h1>']; return $data; }
/** * 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 Markup message. drupal_set_message(Markup::create('Markup with <em>markup!</em>')); // Test duplicate Markup messages. drupal_set_message(Markup::create('Markup with <em>markup!</em>')); // Ensure that multiple Markup messages work. drupal_set_message(Markup::create('Markup2 with <em>markup!</em>')); // Test mixing of types. drupal_set_message(Markup::create('Non duplicate Markup / string.')); drupal_set_message('Non duplicate Markup / string.'); drupal_set_message(Markup::create('Duplicate Markup / string.'), 'status', TRUE); drupal_set_message('Duplicate Markup / string.', 'status', TRUE); // Test auto-escape of non safe strings. drupal_set_message('<em>This<span>markup will be</span> escaped</em>.'); return []; }
public function providerTestAttributeValues() { $data = []; $string = '"> <script>alert(123)</script>"'; $data['safe-object-xss1'] = [['title' => Markup::create($string)], ' title=""> alert(123)""']; $data['non-safe-object-xss1'] = [['title' => $string], ' title="' . Html::escape($string) . '"']; $string = '"><script>alert(123)</script>'; $data['safe-object-xss2'] = [['title' => Markup::create($string)], ' title="">alert(123)"']; $data['non-safe-object-xss2'] = [['title' => $string], ' title="' . Html::escape($string) . '"']; return $data; }
/** * Tests the link_generator Twig functions. */ public function testTwigLinkGenerator() { $this->drupalGet('twig-theme-test/link-generator'); /** @var \Drupal\Core\Utility\LinkGenerator $link_generator */ $link_generator = $this->container->get('link_generator'); $generated_url = Url::fromRoute('user.register', [], ['absolute' => TRUE])->toString(TRUE)->getGeneratedUrl(); $expected = [ 'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register', [], ['absolute' => TRUE])), 'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register', [], ['absolute' => TRUE, 'attributes' => ['foo' => 'bar']])), 'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register', [], ['attributes' => ['foo' => 'bar', 'id' => 'kitten']])), 'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register', [], ['attributes' => ['id' => 'kitten']])), 'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register', [], ['attributes' => ['class' => ['llama', 'kitten', 'panda']]])), 'link via the linkgenerator: ' . $link_generator->generate(Markup::create('<span>register</span>'), new Url('user.register', [], ['absolute' => TRUE])), 'link via the linkgenerator: <a href="' . $generated_url . '"><span>register</span><svg></svg></a>', ]; // Verify that link() has the ability to bubble cacheability metadata: // absolute URLs should bubble the 'url.site' cache context. (This only // needs to test that cacheability metadata is bubbled *at all*; detailed // tests for *which* cacheability metadata is bubbled live elsewhere.) $this->assertCacheContext('url.site'); $content = $this->getRawContent(); $this->assertFalse(empty($content), 'Page content is not empty'); foreach ($expected as $string) { $this->assertRaw('<div>' . $string . '</div>'); } }
function sectionRender($section) { if (empty($section->regions) || $section->key == 'unassigned') { return ''; } $content = ''; foreach ($section->regions as $region) { if ($region_content = $this->regionRender($region)) { $content .= $region_content; } } if ($content) { $render_array = ['#theme' => 'inv-section', '#content' => ['#markup' => Markup::create($content)], '#section' => $section, '#attributes' => ['id' => Html::getId('section-' . $section->key), 'class' => [Html::getClass('inv-section')]]]; return drupal_render($render_array); } return ""; }
/** * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses. * * @param string $html * HTML markup. * @param array $no_js_placeholders * Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe * selectors. * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering no-JS * BigPipe placeholders. */ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) { // Split the HTML on every no-JS placeholder string. $prepare_for_preg_split = function ($placeholder_string) { return '(' . preg_quote($placeholder_string, '/') . ')'; }; $preg_placeholder_strings = array_map($prepare_for_preg_split, array_keys($no_js_placeholders)); $fragments = preg_split('/' . implode('|', $preg_placeholder_strings) . '/', $html, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); foreach ($fragments as $fragment) { // If the fragment isn't one of the no-JS placeholders, it is the HTML in // between placeholders and it must be printed & flushed immediately. The // rest of the logic in the loop handles the placeholders. if (!isset($no_js_placeholders[$fragment])) { print $fragment; flush(); continue; } $placeholder = $fragment; assert('isset($no_js_placeholders[$placeholder])'); $token = Crypt::randomBytesBase64(55); // Render the placeholder, but include the cumulative settings assets, so // we can calculate the overall settings for the entire page. $placeholder_plus_cumulative_settings = ['placeholder' => $no_js_placeholders[$placeholder], 'cumulative_settings_' . $token => ['#attached' => ['drupalSettings' => $cumulative_assets->getSettings()]]]; $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings); // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent // before the HTML they're associated with. In other words: ensure the // critical assets for this placeholder's markup are loaded first. // @see \Drupal\Core\Render\HtmlResponseSubscriber // @see template_preprocess_html() $css_placeholder = '<nojs-bigpipe-placeholder-styles-placeholder token="' . $token . '">'; $js_placeholder = '<nojs-bigpipe-placeholder-scripts-placeholder token="' . $token . '">'; $elements['#markup'] = Markup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']); $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder; $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder; $html_response = new HtmlResponse(); $html_response->setContent($elements); $html_response->getCacheableMetadata()->setCacheMaxAge(0); // Push a fake request with the asset libraries loaded so far and dispatch // KernelEvents::RESPONSE event. This results in the attachments for the // HTML response being processed by HtmlResponseAttachmentsProcessor and // hence: // - the HTML to load the CSS can be rendered. // - the HTML to load the JS (at the top) can be rendered. $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())]); $this->requestStack->push($fake_request); $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $html_response); $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event); $html_response = $event->getResponse(); $this->requestStack->pop(); // Send this embedded HTML response. print $html_response->getContent(); flush(); // Another placeholder was rendered and sent, track the set of asset // libraries sent so far. Any new settings also need to be tracked, so // they can be sent in ::sendPreBody(). // @todo What if drupalSettings already was printed in the HTML <head>? That case is not yet handled. In that case, no-JS BigPipe would cause broken (incomplete) drupalSettings… This would not matter if it were only used if JS is not enabled, but that's not the only use case. However, this $cumulative_assets->setAlreadyLoadedLibraries(array_merge($cumulative_assets->getAlreadyLoadedLibraries(), $html_response->getAttachments()['library'])); $cumulative_assets->setSettings($html_response->getAttachments()['drupalSettings']); } }
/** * Test tokens on node with the token view mode overriding default formatters. */ public function testTokenViewMode() { $value = 'A really long string that should be trimmed by the special formatter on token view we are going to have.'; // The formatter we are going to use will eventually call Unicode::strlen. // This expects that the Unicode has already been explicitly checked, which // happens in DrupalKernel. But since that doesn't run in kernel tests, we // explicitly call this here. Unicode::check(); // Create a node with a value in the text field and test its token. $entity = Node::create(['title' => 'Test node title', 'type' => 'article', 'test_field' => ['value' => $value, 'format' => $this->testFormat->id()]]); $entity->save(); $this->assertTokens('node', ['node' => $entity], ['test_field' => Markup::create($value)]); // Now, create a token view mode which sets a different format for // test_field. When replacing tokens, this formatter should be picked over // the default formatter for the field type. // @see field_tokens(). $view_mode = EntityViewMode::create(['id' => 'node.token', 'targetEntityType' => 'node']); $view_mode->save(); $entity_display = entity_get_display('node', 'article', 'token'); $entity_display->setComponent('test_field', ['type' => 'text_trimmed', 'settings' => ['trim_length' => 50]]); $entity_display->save(); $this->assertTokens('node', ['node' => $entity], ['test_field' => Markup::create(substr($value, 0, 50))]); }
/** * {@inheritdoc} */ public function getSettingsLink() { $url = URL::fromRoute('ckeditor_media_embed.ckeditor_media_embed_settings_form', array('destination' => \Drupal::service('path.current')->getPath())); return Markup::create(\Drupal::l($this->t('CKEditor Media Embed plugin settings page'), $url)); }
/** * {@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 = Markup::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 MarkupInterface ? $output : (string) $output; }
/** * 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)->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-5')); $this->urlGenerator->expects($this->at(1))->method('generateFromRoute')->with('test_route_5', array(), $this->defaultOptions)->willReturn((new GeneratedUrl())->setGeneratedUrl('/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 // Markup::create() call is an intentional unit test for the interaction // between MarkupInterface and the LinkGenerator. $url = new Url('test_route_5', array()); $url->setUrlGenerator($this->urlGenerator); $result = $this->linkGenerator->generate(Markup::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\Render\MarkupInterface|string * The rendered inline template as a Markup 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 Markup::create($this->loadTemplate($template_string, NULL)->render($context)); }
/** * Provide test examples. */ public function providerTestMarkupInterfaceEmpty() { return ['empty TranslatableMarkup' => ['', new TranslatableMarkup('')], 'non-empty TranslatableMarkup' => ['<span>test</span>', new TranslatableMarkup('test')], 'empty FormattableMarkup' => ['', new FormattableMarkup('', ['@foo' => 'bar'])], 'non-empty FormattableMarkup' => ['<span>bar</span>', new FormattableMarkup('@foo', ['@foo' => 'bar'])], 'non-empty Markup' => ['<span>test</span>', Markup::create('test')], 'empty GeneratedLink' => ['', new GeneratedLink()], 'non-empty GeneratedLink' => ['<span><a hef="http://www.example.com">test</a></span>', (new GeneratedLink())->setGeneratedLink('<a hef="http://www.example.com">test</a>')], 'empty SafeMarkupTestMarkup' => ['<span></span>', SafeMarkupTestMarkup::create('')], 'non-empty SafeMarkupTestMarkup' => ['<span>test</span>', SafeMarkupTestMarkup::create('test')]]; }
/** * Provide test examples. */ public function providerTestThemeRenderAndAutoescape() { return ['empty string unchanged' => ['', ''], 'simple string unchanged' => ['ab', 'ab'], 'int (scalar) cast to string' => [111, '111'], 'float (scalar) cast to string' => [2.1, '2.10'], '> is escaped' => ['>', '>'], 'Markup EM tag is unchanged' => [Markup::create('<em>hi</em>'), '<em>hi</em>'], 'Markup SCRIPT tag is unchanged' => [Markup::create('<script>alert("hi");</script>'), '<script>alert("hi");</script>'], 'EM tag in string is escaped' => ['<em>hi</em>', Html::escape('<em>hi</em>')], 'type link render array is rendered' => [['#type' => 'link', '#title' => 'Text', '#url' => '<none>'], '<a href="">Text</a>'], 'type markup with EM tags is rendered' => [['#markup' => '<em>hi</em>'], '<em>hi</em>'], 'SCRIPT tag in string is escaped' => ['<script>alert(123)</script>', Html::escape('<script>alert(123)</script>')], 'type plain_text render array EM tag is escaped' => [['#plain_text' => '<em>hi</em>'], Html::escape('<em>hi</em>')], 'type hidden render array is rendered' => [['#type' => 'hidden', '#name' => 'foo', '#value' => 'bar'], "<input type=\"hidden\" name=\"foo\" value=\"bar\" />\n"]]; }
/** * 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*8 = 16 permutations. (On one axis: uncacheable vs. * uncacheable = 2; on the other axis: A1–7 and B = 8.) * * @todo Case A5 is not yet supported by core. So that makes for only 14 * permutations currently, instead of 16. That will be done in * https://www.drupal.org/node/2559847 * * @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\Markup::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 Markup::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', presence of max-age=0 created // by the #lazy_builder callback. // @todo in https://www.drupal.org/node/2559847 $base_element_a5 = []; // Note the absence of '#create_placeholder', presence of high cardinality // cache context created by the #lazy_builder callback. // @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser() $base_element_a6 = ['#attached' => ['drupalSettings' => ['foo' => 'bar']], 'placeholder' => ['#cache' => ['contexts' => []], '#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackPerUser', $args]]]; // Note the absence of '#create_placeholder', presence of high-invalidation // frequency cache tag created by the #lazy_builder callback. // @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature() $base_element_a7 = ['#attached' => ['drupalSettings' => ['foo' => 'bar']], 'placeholder' => ['#cache' => ['contexts' => []], '#lazy_builder' => ['Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackTagCurrentTemperature', $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 DOES NOT have a placeholder that is: // - NOT created, despite max-age=0 that is bubbled // - uncacheable // (because the render element with #lazy_builder does not have #cache[keys] // and hence the max-age=0 bubbles up further) // @todo in https://www.drupal.org/node/2559847 // Case ten: render array that has a placeholder that is: // - automatically created, and automatically triggered due to max-age=0 // that is bubbled // - cacheable // @todo in https://www.drupal.org/node/2559847 // Case eleven: render array that DOES NOT have a placeholder that is: // - NOT created, despite high cardinality cache contexts that are bubbled // - uncacheable $element_without_cache_keys = $base_element_a6; $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a6['placeholder']); $cases[] = [$element_without_cache_keys, $args, $expected_placeholder_render_array, FALSE, ['user'], [], []]; // Case twelve: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // cardinality cache contexts that are bubbled // - cacheable $element_with_cache_keys = $base_element_a6; $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, ['user'], [], ['#markup' => '<p>This is a rendered placeholder!</p>', '#attached' => ['drupalSettings' => ['dynamic_animal' => $args[0]]], '#cache' => ['contexts' => ['user'], 'tags' => [], 'max-age' => Cache::PERMANENT]]]; // Case thirteen: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // invalidation frequency cache tags that are bubbled // - uncacheable $element_without_cache_keys = $base_element_a7; $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a7['placeholder']); $cases[] = [$element_without_cache_keys, $args, $expected_placeholder_render_array, FALSE, [], ['current-temperature'], []]; // Case fourteen: render array that has a placeholder that is: // - automatically created, and automatically triggered due to high // invalidation frequency cache tags that are bubbled // - cacheable $element_with_cache_keys = $base_element_a7; $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 fifteen: 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 sixteen: 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; }
/** * #lazy_builder callback; says "hello" or "yarhar". * * @return array */ public static function helloOrYarhar() { return ['#markup' => Markup::create('<marquee>Yarhar llamas forever!</marquee>'), '#cache' => ['max-age' => 0]]; }
public function providerCastSafeStrings() { $safe_string = Markup::create('test safe string'); return [['test simple string', 'test simple string'], [['test simple array', 'test simple array'], ['test simple array', 'test simple array']], ['test safe string', $safe_string], [['test safe string', 'test safe string'], [$safe_string, $safe_string]], [['test safe string', 'mixed array', 'test safe string'], [$safe_string, 'mixed array', $safe_string]]]; }
/** * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses. * * @param string $html * HTML markup. * @param array $no_js_placeholders * Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe * selectors. * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering no-JS * BigPipe placeholders. */ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) { $fragments = explode('<div data-big-pipe-selector-nojs="', $html); print array_shift($fragments); ob_end_flush(); flush(); foreach ($fragments as $fragment) { $t = explode('"></div>', $fragment, 2); $placeholder = $t[0]; if (!isset($no_js_placeholders[$placeholder])) { continue; } $token = Crypt::randomBytesBase64(55); // Render the placeholder, but include the cumulative settings assets, so // we can calculate the overall settings for the entire page. $placeholder_plus_cumulative_settings = ['placeholder' => $no_js_placeholders[$placeholder], 'cumulative_settings_' . $token => ['#attached' => ['drupalSettings' => $cumulative_assets->getSettings()]]]; $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings); // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent // before the HTML they're associated with. In other words: ensure the // critical assets for this placeholder's markup are loaded first. // @see \Drupal\Core\Render\HtmlResponseSubscriber // @see template_preprocess_html() $css_placeholder = '<nojs-bigpipe-placeholder-styles-placeholder token="' . $token . '">'; $js_placeholder = '<nojs-bigpipe-placeholder-scripts-placeholder token="' . $token . '">'; $elements['#markup'] = Markup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']); $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder; $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder; $html_response = new HtmlResponse(); $html_response->setContent($elements); $html_response->getCacheableMetadata()->setCacheMaxAge(0); // Push a fake request with the asset libraries loaded so far and dispatch // KernelEvents::RESPONSE event. This results in the attachments for the // HTML response being processed by HtmlResponseAttachmentsProcessor and // hence: // - the HTML to load the CSS can be rendered. // - the HTML to load the JS (at the top) can be rendered. $fake_request = $this->requestStack->getMasterRequest()->duplicate(); $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']); $this->requestStack->push($fake_request); $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $html_response); $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event); $html_response = $event->getResponse(); $this->requestStack->pop(); // Send this embedded HTML response. print $html_response->getContent(); print $t[1]; flush(); // Another placeholder was rendered and sent, track the set of asset // libraries sent so far. Any new settings also need to be tracked, so // they can be sent in ::sendPreBody(). // @todo What if drupalSettings already was printed in the HTML <head>? That case is not yet handled. In that case, no-JS BigPipe would cause broken (incomplete) drupalSettings… This would not matter if it were only used if JS is not enabled, but that's not the only use case. However, this $final_settings = $html_response->getAttachments()['drupalSettings']; $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $final_settings['ajaxPageState']['libraries'])); $cumulative_assets->setSettings($final_settings); } }
/** * @covers ::setData * @covers ::set * @covers ::initWithData */ public function testSafeStringHandling() { // Safe strings are cast when using ::set(). $safe_string = Markup::create('bar'); $this->config->set('foo', $safe_string); $this->assertSame('bar', $this->config->get('foo')); $this->config->set('foo', ['bar' => $safe_string]); $this->assertSame('bar', $this->config->get('foo.bar')); // Safe strings are cast when using ::setData(). $this->config->setData(['bar' => $safe_string]); $this->assertSame('bar', $this->config->get('bar')); // Safe strings are not cast when using ::initWithData(). $this->config->initWithData(['bar' => $safe_string]); $this->assertSame($safe_string, $this->config->get('bar')); }
public function testEntityReferenceTokens() { $reference = Node::create(['title' => 'Test node to reference', 'type' => 'article', 'test_field' => ['value' => 'foo', 'format' => $this->testFormat->id()]]); $reference->save(); $term_reference_field_value = $this->randomString(); $term_reference = $this->createTerm($this->vocabulary, ['name' => 'Term to reference', 'term_field' => ['value' => $term_reference_field_value, 'format' => $this->testFormat->id()]]); $entity = Node::create(['title' => 'Test entity reference', 'type' => 'article', 'test_reference' => ['target_id' => $reference->id()], 'test_term_reference' => ['target_id' => $term_reference->id()]]); $entity->save(); $this->assertTokens('node', ['node' => $entity], ['test_reference:entity:title' => Markup::create('Test node to reference'), 'test_reference:entity:test_field' => Markup::create('foo'), 'test_term_reference:entity:term_field' => Html::escape($term_reference_field_value), 'test_reference:target_id' => $reference->id(), 'test_term_reference:target_id' => $term_reference->id(), 'test_term_reference:entity:url:path' => '/' . $term_reference->toUrl('canonical')->getInternalPath(), 'test_reference:entity' => $reference->label(), 'test_term_reference:entity' => $term_reference->label()]); // Test some non existent tokens. $this->assertNoTokens('node', ['node' => $entity], ['test_reference:1:title', 'test_reference:entity:does_not_exist', 'test_reference:does_not:exist', 'test_term_reference:does_not_exist', 'test_term_reference:does:not:exist', 'test_term_reference:does_not_exist:0', 'non_existing_field:entity:title']); /** @var \Drupal\token\Token $token_service */ $token_service = \Drupal::service('token'); $token_info = $token_service->getTokenInfo('node', 'test_reference'); $this->assertEquals('Test reference', $token_info['name']); $this->assertEquals('Entity reference field.', (string) $token_info['description']); $this->assertEquals('token', $token_info['module']); $this->assertEquals('node-test_reference', $token_info['type']); // Test target_id field property token info. $token_info = $token_service->getTokenInfo('node-test_reference', 'target_id'); $this->assertEquals('Content ID', $token_info['name']); $this->assertEquals('token', $token_info['module']); $this->assertEquals('token', $token_info['module']); // Test entity field property token info. $token_info = $token_service->getTokenInfo('node-test_reference', 'entity'); $this->assertEquals('Content', $token_info['name']); $this->assertEquals('The referenced entity', $token_info['description']); $this->assertEquals('token', $token_info['module']); $this->assertEquals('node', $token_info['type']); // Test entity field property token info of the term reference. $token_info = $token_service->getTokenInfo('node-test_term_reference', 'entity'); $this->assertEquals('Taxonomy term', $token_info['name']); $this->assertEquals('The referenced entity', $token_info['description']); $this->assertEquals('token', $token_info['module']); $this->assertEquals('term', $token_info['type']); }