/** * 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"); } }
/** * Tests the rediscovering. */ public function testRediscover() { \Drupal::state()->set('menu_link_content_dynamic_route.routes', ['route_name_1' => new Route('/example-path')]); \Drupal::service('router.builder')->rebuild(); // Set up a custom menu link pointing to a specific path. MenuLinkContent::create(['title' => '<script>alert("Welcome to the discovered jungle!")</script>', 'link' => [['uri' => 'internal:/example-path']], 'menu_name' => 'tools'])->save(); $menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters()); $this->assertEqual(1, count($menu_tree)); /** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */ $tree_element = reset($menu_tree); $this->assertEqual('route_name_1', $tree_element->link->getRouteName()); // Change the underlying route and trigger the rediscovering. \Drupal::state()->set('menu_link_content_dynamic_route.routes', ['route_name_2' => new Route('/example-path')]); \Drupal::service('router.builder')->rebuild(); // Ensure that the new route name / parameters are captured by the tree. $menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters()); $this->assertEqual(1, count($menu_tree)); /** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */ $tree_element = reset($menu_tree); $this->assertEqual('route_name_2', $tree_element->link->getRouteName()); $title = $tree_element->link->getTitle(); $this->assertFalse($title instanceof TranslationWrapper); $this->assertIdentical('<script>alert("Welcome to the discovered jungle!")</script>', $title); $this->assertFalse(SafeMarkup::isSafe($title)); }
/** * Tests the listing of displays on a views list builder. * * @see \Drupal\views_ui\ViewListBuilder::getDisplaysList() * @covers ::buildRow */ public function testBuildRowEntityList() { $storage = $this->getMockBuilder('Drupal\\Core\\Config\\Entity\\ConfigEntityStorage')->disableOriginalConstructor()->getMock(); $display_manager = $this->getMockBuilder('\\Drupal\\views\\Plugin\\ViewsPluginManager')->disableOriginalConstructor()->getMock(); $display_manager->expects($this->any())->method('getDefinition')->will($this->returnValueMap(array(array('default', TRUE, array('id' => 'default', 'title' => 'Master', 'theme' => 'views_view', 'no_ui' => TRUE, 'admin' => '')), array('page', TRUE, array('id' => 'page', 'title' => 'Page', 'uses_menu_links' => TRUE, 'uses_route' => TRUE, 'contextual_links_locations' => array('page'), 'theme' => 'views_view', 'admin' => 'Page admin label')), array('embed', TRUE, array('id' => 'embed', 'title' => 'embed', 'theme' => 'views_view', 'admin' => 'Embed admin label'))))); $default_display = $this->getMock('Drupal\\views\\Plugin\\views\\display\\DefaultDisplay', array('initDisplay'), array(array(), 'default', $display_manager->getDefinition('default'))); $route_provider = $this->getMock('Drupal\\Core\\Routing\\RouteProviderInterface'); $state = $this->getMock('\\Drupal\\Core\\State\\StateInterface'); $menu_storage = $this->getMock('\\Drupal\\Core\\Entity\\EntityStorageInterface'); $page_display = $this->getMock('Drupal\\views\\Plugin\\views\\display\\Page', array('initDisplay', 'getPath'), array(array(), 'default', $display_manager->getDefinition('page'), $route_provider, $state, $menu_storage)); $page_display->expects($this->any())->method('getPath')->will($this->onConsecutiveCalls($this->returnValue('test_page'), $this->returnValue('<object>malformed_path</object>'), $this->returnValue('<script>alert("placeholder_page/%")</script>'))); $embed_display = $this->getMock('Drupal\\views\\Plugin\\views\\display\\Embed', array('initDisplay'), array(array(), 'default', $display_manager->getDefinition('embed'))); $values = array(); $values['status'] = FALSE; $values['display']['default']['id'] = 'default'; $values['display']['default']['display_title'] = 'Display'; $values['display']['default']['display_plugin'] = 'default'; $values['display']['page_1']['id'] = 'page_1'; $values['display']['page_1']['display_title'] = 'Page 1'; $values['display']['page_1']['display_plugin'] = 'page'; $values['display']['page_1']['display_options']['path'] = 'test_page'; $values['display']['page_2']['id'] = 'page_2'; $values['display']['page_2']['display_title'] = 'Page 2'; $values['display']['page_2']['display_plugin'] = 'page'; $values['display']['page_2']['display_options']['path'] = '<object>malformed_path</object>'; $values['display']['page_3']['id'] = 'page_3'; $values['display']['page_3']['display_title'] = 'Page 3'; $values['display']['page_3']['display_plugin'] = 'page'; $values['display']['page_3']['display_options']['path'] = '<script>alert("placeholder_page/%")</script>'; $values['display']['embed']['id'] = 'embed'; $values['display']['embed']['display_title'] = 'Embedded'; $values['display']['embed']['display_plugin'] = 'embed'; $display_manager->expects($this->any())->method('createInstance')->will($this->returnValueMap(array(array('default', $values['display']['default'], $default_display), array('page', $values['display']['page_1'], $page_display), array('page', $values['display']['page_2'], $page_display), array('page', $values['display']['page_3'], $page_display), array('embed', $values['display']['embed'], $embed_display)))); $container = new ContainerBuilder(); $user = $this->getMock('Drupal\\Core\\Session\\AccountInterface'); $request_stack = new RequestStack(); $request_stack->push(new Request()); $views_data = $this->getMockBuilder('Drupal\\views\\ViewsData')->disableOriginalConstructor()->getMock(); $route_provider = $this->getMock('Drupal\\Core\\Routing\\RouteProviderInterface'); $executable_factory = new ViewExecutableFactory($user, $request_stack, $views_data, $route_provider); $container->set('views.executable', $executable_factory); $container->set('plugin.manager.views.display', $display_manager); \Drupal::setContainer($container); // Setup a view list builder with a mocked buildOperations method, // because t() is called on there. $entity_type = $this->getMock('Drupal\\Core\\Entity\\EntityTypeInterface'); $view_list_builder = new TestViewListBuilder($entity_type, $storage, $display_manager); $view_list_builder->setStringTranslation($this->getStringTranslationStub()); $view = new View($values, 'view'); $row = $view_list_builder->buildRow($view); $expected_displays = array('Embed admin label', 'Page admin label', 'Page admin label', 'Page admin label'); $this->assertEquals($expected_displays, $row['data']['view_name']['data']['#displays']); $display_paths = $row['data']['path']['data']['#items']; // These values will be escaped by Twig when rendered. $this->assertEquals('/test_page, /<object>malformed_path</object>, /<script>alert("placeholder_page/%")</script>', implode(', ', $display_paths)); $this->assertFalse(SafeMarkup::isSafe('/<object>malformed_path</object>'), '/<script>alert("/<object>malformed_path</object> is not marked safe.'); $this->assertFalse(SafeMarkup::isSafe('/<script>alert("placeholder_page/%")'), '/<script>alert("/<script>alert("placeholder_page/%") is not marked safe.'); }
/** * @dataProvider providerTestFormatPlural */ public function testFormatPlural($count, $singular, $plural, array $args = array(), array $options = array(), $expected) { $translator = $this->getMock('\\Drupal\\Core\\StringTranslation\\Translator\\TranslatorInterface'); $translator->expects($this->once())->method('getStringTranslation')->will($this->returnCallback(function ($langcode, $string) { return $string; })); $this->translationManager->addTranslator($translator); $result = $this->translationManager->formatPlural($count, $singular, $plural, $args, $options); $this->assertEquals($expected, $result); $this->assertTrue(SafeMarkup::isSafe($result)); }
/** * Tests the rediscovering. */ public function testRediscover() { \Drupal::state()->set('menu_link_content_dynamic_route.routes', ['route_name_1' => new Route('/example-path')]); \Drupal::service('router.builder')->rebuild(); // Set up a custom menu link pointing to a specific path. $parent = MenuLinkContent::create(['title' => '<script>alert("Welcome to the discovered jungle!")</script>', 'link' => [['uri' => 'internal:/example-path']], 'menu_name' => 'tools']); $parent->save(); $menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters()); $this->assertEqual(1, count($menu_tree)); /** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */ $tree_element = reset($menu_tree); $this->assertEqual('route_name_1', $tree_element->link->getRouteName()); // Change the underlying route and trigger the rediscovering. \Drupal::state()->set('menu_link_content_dynamic_route.routes', ['route_name_2' => new Route('/example-path')]); \Drupal::service('router.builder')->rebuild(); // Ensure that the new route name / parameters are captured by the tree. $menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters()); $this->assertEqual(1, count($menu_tree)); /** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */ $tree_element = reset($menu_tree); $this->assertEqual('route_name_2', $tree_element->link->getRouteName()); $title = $tree_element->link->getTitle(); $this->assertFalse($title instanceof TranslatableMarkup); $this->assertIdentical('<script>alert("Welcome to the discovered jungle!")</script>', $title); $this->assertFalse(SafeMarkup::isSafe($title)); // Create a hierarchy. \Drupal::state()->set('menu_link_content_dynamic_route.routes', ['route_name_1' => new Route('/example-path'), 'route_name_2' => new Route('/example-path/child')]); $child = MenuLinkContent::create(['title' => 'Child', 'link' => [['uri' => 'entity:/example-path/child']], 'menu_name' => 'tools', 'parent' => 'menu_link_content:' . $parent->uuid()]); $child->save(); $parent->set('link', [['uri' => 'entity:/example-path']]); $parent->save(); $menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters()); $this->assertEqual(1, count($menu_tree)); /** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */ $tree_element = reset($menu_tree); $this->assertTrue($tree_element->hasChildren); $this->assertEqual(1, count($tree_element->subtree)); // Edit child element link to use 'internal' instead of 'entity'. $child->set('link', [['uri' => 'internal:/example-path/child']]); $child->save(); \Drupal::service('plugin.manager.menu.link')->rebuild(); $menu_tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters()); $this->assertEqual(1, count($menu_tree)); /** @var \Drupal\Core\Menu\MenuLinkTreeElement $tree_element */ $tree_element = reset($menu_tree); $this->assertTrue($tree_element->hasChildren); $this->assertEqual(1, count($tree_element->subtree)); }
/** * Tests comment preview. */ function testCommentPreview() { // As admin user, configure comment settings. $this->drupalLogin($this->adminUser); $this->setCommentPreview(DRUPAL_OPTIONAL); $this->setCommentForm(TRUE); $this->setCommentSubject(TRUE); $this->setCommentSettings('default_mode', CommentManagerInterface::COMMENT_MODE_THREADED, 'Comment paging changed.'); $this->drupalLogout(); // Login as web user. $this->drupalLogin($this->webUser); // Test escaping of the username on the preview form. \Drupal::service('module_installer')->install(['user_hooks_test']); \Drupal::state()->set('user_hooks_test_user_format_name_alter', TRUE); $edit = array(); $edit['subject[0][value]'] = $this->randomMachineName(8); $edit['comment_body[0][value]'] = $this->randomMachineName(16); $this->drupalPostForm('node/' . $this->node->id(), $edit, t('Preview')); $this->assertEscaped('<em>' . $this->webUser->id() . '</em>'); \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE); $this->drupalPostForm('node/' . $this->node->id(), $edit, t('Preview')); $this->assertTrue(SafeMarkup::isSafe($this->webUser->getDisplayName()), 'Username is marked safe'); $this->assertNoEscaped('<em>' . $this->webUser->id() . '</em>'); $this->assertRaw('<em>' . $this->webUser->id() . '</em>'); // Add a user picture. $image = current($this->drupalGetTestFiles('image')); $user_edit['files[user_picture_0]'] = drupal_realpath($image->uri); $this->drupalPostForm('user/' . $this->webUser->id() . '/edit', $user_edit, t('Save')); // As the web user, fill in the comment form and preview the comment. $this->drupalPostForm('node/' . $this->node->id(), $edit, t('Preview')); // Check that the preview is displaying the title and body. $this->assertTitle(t('Preview comment | Drupal'), 'Page title is "Preview comment".'); $this->assertText($edit['subject[0][value]'], 'Subject displayed.'); $this->assertText($edit['comment_body[0][value]'], 'Comment displayed.'); // Check that the title and body fields are displayed with the correct values. $this->assertFieldByName('subject[0][value]', $edit['subject[0][value]'], 'Subject field displayed.'); $this->assertFieldByName('comment_body[0][value]', $edit['comment_body[0][value]'], 'Comment field displayed.'); // Check that the user picture is displayed. $this->assertFieldByXPath("//article[contains(@class, 'preview')]//div[contains(@class, 'user-picture')]//img", NULL, 'User picture displayed.'); }
/** * Tests how hook_link_alter() can affect escaping of the link text. */ function testHookLinkAlter() { $url = Url::fromUri('http://example.com'); $renderer = \Drupal::service('renderer'); $link = $renderer->executeInRenderContext(new RenderContext(), function () use($url) { return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url); }); $this->setRawContent($link); $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.'); // Ensure the content of the link is not escaped. $this->assertRaw('<em>link with markup</em>'); // Test just adding text to an already safe string. \Drupal::state()->set('link_generation_test_link_alter', TRUE); $link = $renderer->executeInRenderContext(new RenderContext(), function () use($url) { return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url); }); $this->setRawContent($link); $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.'); // Ensure the content of the link is escaped. $this->assertEscaped('<em>link with markup</em> <strong>Test!</strong>'); // Test passing a safe string to t(). \Drupal::state()->set('link_generation_test_link_alter_safe', TRUE); $link = $renderer->executeInRenderContext(new RenderContext(), function () use($url) { return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url); }); $this->setRawContent($link); $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.'); // Ensure the content of the link is escaped. $this->assertRaw('<em>link with markup</em> <strong>Test!</strong>'); // Test passing an unsafe string to t(). $link = $renderer->executeInRenderContext(new RenderContext(), function () use($url) { return \Drupal::l('<em>link with markup</em>', $url); }); $this->setRawContent($link); $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.'); // Ensure the content of the link is escaped. $this->assertEscaped('<em>link with markup</em>'); $this->assertRaw('<strong>Test!</strong>'); }
/** * Creates the different types of attribute values. * * @param string $name * The attribute name. * @param mixed $value * The attribute value. * * @return \Drupal\Core\Template\AttributeValueBase * An AttributeValueBase representation of the attribute's value. */ protected function createAttributeValue($name, $value) { // If the value is already an AttributeValueBase object, // return a new instance of the same class, but with the new name. if ($value instanceof AttributeValueBase) { $class = get_class($value); return new $class($name, $value->value()); } // An array value or 'class' attribute name are forced to always be an // AttributeArray value for consistency. if ($name == 'class' && !is_array($value)) { // Cast the value to string in case it implements MarkupInterface. $value = [(string) $value]; } if (is_array($value)) { // Cast the value to an array if the value was passed in as a string. // @todo Decide to fix all the broken instances of class as a string // in core or cast them. $value = new AttributeArray($name, $value); } elseif (is_bool($value)) { $value = new AttributeBoolean($name, $value); } elseif (SafeMarkup::isSafe($value)) { // Attributes are not supposed to display HTML markup, so we just convert // the value to plain text. $value = PlainTextOutput::renderFromHtml($value); $value = new AttributeString($name, $value); } elseif (!is_object($value)) { $value = new AttributeString($name, $value); } return $value; }
/** * {@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 Markup 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] = Markup::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' => Markup::create($cacheable_items[$key]['#markup'])]; } } $data += $cacheable_items; } $data['#markup'] = Markup::create($data['#markup']); return $data; }
/** * Tests string formatting with SafeMarkup::format(). * * @dataProvider providerFormat * @covers ::format * * @param string $string * The string to run through SafeMarkup::format(). * @param string[] $args * The arguments to pass into SafeMarkup::format(). * @param string $expected * The expected result from calling the function. * @param string $message * The message to display as output to the test. * @param bool $expected_is_safe * Whether the result is expected to be safe for HTML display. */ public function testFormat($string, array $args, $expected, $message, $expected_is_safe) { UrlHelper::setAllowedProtocols(['http', 'https', 'mailto']); $result = SafeMarkup::format($string, $args); $this->assertEquals($expected, $result, $message); $this->assertEquals($expected_is_safe, SafeMarkup::isSafe($result), 'SafeMarkup::format correctly sets the result as safe or not safe.'); foreach ($args as $arg) { $this->assertSame($arg instanceof SafeMarkupTestMarkup, SafeMarkup::isSafe($arg)); } }
/** * 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; }
/** * 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); }
/** * 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\Render\MarkupInterface|string * The escaped markup wrapped in a Markup 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'] = Markup::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'] = Markup::create(Xss::filter($elements['#markup'], $tags)); } return $elements; }
/** * Replaces all tokens in a given string with appropriate values. * * @param string $text * An HTML string containing replaceable tokens. The caller is responsible * for calling \Drupal\Component\Utility\Html::escape() in case the $text * was plain text. * @param array $data * (optional) An array of keyed objects. For simple replacement scenarios * 'node', 'user', and others are common keys, with an accompanying node or * user object being the value. Some token types, like 'site', do not require * any explicit information from $data and can be replaced even if it is * empty. * @param array $options * (optional) A keyed array of settings and flags to control the token * replacement process. Supported options are: * - langcode: A language code to be used when generating locale-sensitive * tokens. * - callback: A callback function that will be used to post-process the * array of token replacements after they are generated. * - clear: A boolean flag indicating that tokens should be removed from the * final text if no replacement value can be generated. * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata|null * (optional) An object to which static::generate() and the hooks and * functions that it invokes will add their required bubbleable metadata. * * To ensure that the metadata associated with the token replacements gets * attached to the same render array that contains the token-replaced text, * callers of this method are encouraged to pass in a BubbleableMetadata * object and apply it to the corresponding render array. For example: * @code * $bubbleable_metadata = new BubbleableMetadata(); * $build['#markup'] = $token_service->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node], [], $bubbleable_metadata); * $bubbleable_metadata->applyTo($build); * @endcode * * When the caller does not pass in a BubbleableMetadata object, this * method creates a local one, and applies the collected metadata to the * Renderer's currently active render context. * * @return string * The token result is the entered HTML text with tokens replaced. The * caller is responsible for choosing the right escaping / sanitization. If * the result is intended to be used as plain text, using * PlainTextOutput::renderFromHtml() is recommended. If the result is just * printed as part of a template relying on Twig autoescaping is possible, * otherwise for example the result can be put into #markup, in which case * it would be sanitized by Xss::filterAdmin(). */ public function replace($text, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata = NULL) { $text_tokens = $this->scan($text); if (empty($text_tokens)) { return $text; } $bubbleable_metadata_is_passed_in = (bool) $bubbleable_metadata; $bubbleable_metadata = $bubbleable_metadata ?: new BubbleableMetadata(); $replacements = array(); foreach ($text_tokens as $type => $tokens) { $replacements += $this->generate($type, $tokens, $data, $options, $bubbleable_metadata); if (!empty($options['clear'])) { $replacements += array_fill_keys($tokens, ''); } } // Escape the tokens, unless they are explicitly markup. foreach ($replacements as $token => $value) { $replacements[$token] = SafeMarkup::isSafe($value) ? $value : Html::escape($value); } // Optionally alter the list of replacement values. if (!empty($options['callback'])) { $function = $options['callback']; $function($replacements, $data, $options, $bubbleable_metadata); } $tokens = array_keys($replacements); $values = array_values($replacements); // If a local $bubbleable_metadata object was created, apply the metadata // it collected to the renderer's currently active render context. if (!$bubbleable_metadata_is_passed_in && $this->renderer->hasRenderContext()) { $build = []; $bubbleable_metadata->applyTo($build); $this->renderer->render($build); } return str_replace($tokens, $values, $text); }
/** * Run database tasks and tests to see if Drupal can run on the database. */ public function runTasks() { // We need to establish a connection before we can run tests. if ($this->connect()) { foreach ($this->tasks as $task) { if (!isset($task['function'])) { $task['function'] = 'runTestQuery'; } if (method_exists($this, $task['function'])) { // Returning false is fatal. No other tasks can run. if (FALSE === call_user_func_array(array($this, $task['function']), $task['arguments'])) { break; } } else { throw new TaskException(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function']))); } } } // Check for failed results and compile message $message = ''; foreach ($this->results as $result => $success) { if (!$success) { $message = SafeMarkup::isSafe($result) ? $result : SafeMarkup::checkPlain($result); } } if (!empty($message)) { $message = SafeMarkup::set('Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider.' . $message); throw new TaskException($message); } }
/** * {@inheritdoc} * * For anonymous users, the "active" class will be calculated on the server, * because most sites serve each anonymous user the same cached page anyway. * For authenticated users, the "active" class will be calculated on the * client (through JavaScript), only data- attributes are added to links to * prevent breaking the render cache. The JavaScript is added in * system_page_attachments(). * * @see system_page_attachments() */ public function generate($text, Url $url) { // Performance: avoid Url::toString() needing to retrieve the URL generator // service from the container. $url->setUrlGenerator($this->urlGenerator); if (is_array($text)) { $text = $this->renderer->render($text); } // Start building a structured representation of our link to be altered later. $variables = array('text' => $text, 'url' => $url, 'options' => $url->getOptions()); // Merge in default options. $variables['options'] += array('attributes' => array(), 'query' => array(), 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE); // Add a hreflang attribute if we know the language of this link's url and // hreflang has not already been set. if (!empty($variables['options']['language']) && !isset($variables['options']['attributes']['hreflang'])) { $variables['options']['attributes']['hreflang'] = $variables['options']['language']->getId(); } // Ensure that query values are strings. array_walk($variables['options']['query'], function (&$value) { if ($value instanceof MarkupInterface) { $value = (string) $value; } }); // Set the "active" class if the 'set_active_class' option is not empty. if (!empty($variables['options']['set_active_class']) && !$url->isExternal()) { // Add a "data-drupal-link-query" attribute to let the // drupal.active-link library know the query in a standardized manner. if (!empty($variables['options']['query'])) { $query = $variables['options']['query']; ksort($query); $variables['options']['attributes']['data-drupal-link-query'] = Json::encode($query); } // Add a "data-drupal-link-system-path" attribute to let the // drupal.active-link library know the path in a standardized manner. if ($url->isRouted() && !isset($variables['options']['attributes']['data-drupal-link-system-path'])) { // @todo System path is deprecated - use the route name and parameters. $system_path = $url->getInternalPath(); // Special case for the front page. $variables['options']['attributes']['data-drupal-link-system-path'] = $system_path == '' ? '<front>' : $system_path; } } // Remove all HTML and PHP tags from a tooltip, calling expensive strip_tags() // only when a quick strpos() gives suspicion tags are present. if (isset($variables['options']['attributes']['title']) && strpos($variables['options']['attributes']['title'], '<') !== FALSE) { $variables['options']['attributes']['title'] = strip_tags($variables['options']['attributes']['title']); } // Allow other modules to modify the structure of the link. $this->moduleHandler->alter('link', $variables); // Move attributes out of options since generateFromRoute() doesn't need // them. Include a placeholder for the href. $attributes = array('href' => '') + $variables['options']['attributes']; unset($variables['options']['attributes']); $url->setOptions($variables['options']); // External URLs can not have cacheable metadata. if ($url->isExternal()) { $generated_link = new GeneratedLink(); $attributes['href'] = $url->toString(FALSE); } else { $generated_url = $url->toString(TRUE); $generated_link = GeneratedLink::createFromObject($generated_url); // The result of the URL generator is a plain-text URL to use as the href // attribute, and it is escaped by \Drupal\Core\Template\Attribute. $attributes['href'] = $generated_url->getGeneratedUrl(); } if (!SafeMarkup::isSafe($variables['text'])) { $variables['text'] = Html::escape($variables['text']); } $attributes = new Attribute($attributes); // This is safe because Attribute does escaping and $variables['text'] is // either rendered or escaped. return $generated_link->setGeneratedLink('<a' . $attributes . '>' . $variables['text'] . '</a>'); }
/** * Tests string formatting with SafeMarkup::format(). * * @dataProvider providerFormat * @covers ::format * * @param string $string * The string to run through SafeMarkup::format(). * @param string[] $args * The arguments to pass into SafeMarkup::format(). * @param string $expected * The expected result from calling the function. * @param string $message * The message to display as output to the test. * @param bool $expected_is_safe * Whether the result is expected to be safe for HTML display. */ public function testFormat($string, array $args, $expected, $message, $expected_is_safe) { $result = SafeMarkup::format($string, $args); $this->assertEquals($expected, $result, $message); $this->assertEquals($expected_is_safe, SafeMarkup::isSafe($result), 'SafeMarkup::format correctly sets the result as safe or not safe.'); foreach ($args as $arg) { $this->assertSame($arg instanceof SafeMarkupTestSafeString, SafeMarkup::isSafe($arg)); } }
/** * {@inheritdoc} */ public function renderText($alter) { // We need to preserve the safeness of the value regardless of the // alterations made by this method. Any alterations or replacements made // within this method need to ensure that at the minimum the result is // XSS admin filtered. See self::renderAltered() as an example that does. $value_is_safe = SafeMarkup::isSafe($this->last_render); // Cast to a string so that empty checks and string functions work as // expected. $value = (string) $this->last_render; if (!empty($alter['alter_text']) && $alter['text'] !== '') { $tokens = $this->getRenderTokens($alter); $value = $this->renderAltered($alter, $tokens); } if (!empty($this->options['alter']['trim_whitespace'])) { $value = trim($value); } // Check if there should be no further rewrite for empty values. $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->isValueEmpty($this->original_value, $this->options['empty_zero']); // Check whether the value is empty and return nothing, so the field isn't rendered. // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE). // For numeric values you can specify whether "0"/0 should be empty. if (($this->options['hide_empty'] && empty($value) || $alter['phase'] != static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) && $this->isValueEmpty($value, $this->options['empty_zero'], FALSE)) { return ''; } // Only in empty phase. if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) { // If we got here then $alter contains the value of "No results text" // and so there is nothing left to do. return ViewsRenderPipelineMarkup::create($value); } if (!empty($alter['strip_tags'])) { $value = strip_tags($value, $alter['preserve_tags']); } $more_link = ''; if (!empty($alter['trim']) && !empty($alter['max_length'])) { $length = strlen($value); $value = $this->renderTrimText($alter, $value); if ($this->options['alter']['more_link'] && strlen($value) < $length) { $tokens = $this->getRenderTokens($alter); $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : $this->t('more'); $more_link_text = strtr(Xss::filterAdmin($more_link_text), $tokens); $more_link_path = $this->options['alter']['more_link_path']; $more_link_path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($more_link_path, $tokens))); // Make sure that paths which were run through URL generation work as // well. $base_path = base_path(); // Checks whether the path starts with the base_path. if (strpos($more_link_path, $base_path) === 0) { $more_link_path = Unicode::substr($more_link_path, Unicode::strlen($base_path)); } // @todo Views should expect and store a leading /. See // https://www.drupal.org/node/2423913. $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path, array('attributes' => array('class' => array('views-more-link'))))); } } if (!empty($alter['nl2br'])) { $value = nl2br($value); } if ($value_is_safe) { $value = ViewsRenderPipelineMarkup::create($value); } $this->last_render_text = $value; if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) { if (!isset($tokens)) { $tokens = $this->getRenderTokens($alter); } $value = $this->renderAsLink($alter, $value, $tokens); } // Preserve whether or not the string is safe. Since $more_link comes from // \Drupal::l(), it is safe to append. Use SafeMarkup::isSafe() here because // renderAsLink() can return both safe and unsafe values. if (SafeMarkup::isSafe($value)) { return ViewsRenderPipelineMarkup::create($value . $more_link); } else { // If the string is not already marked safe, it is still OK to return it // because it will be sanitized by Twig. return $value . $more_link; } }
/** * Escapes a placeholder replacement value if needed. * * @param string|\Drupal\Component\Render\MarkupInterface $value * A placeholder replacement value. * * @return string * The properly escaped replacement value. */ protected static function placeholderEscape($value) { return SafeMarkup::isSafe($value) ? (string) $value : Html::escape($value); }
/** * Overrides twig_escape_filter(). * * Replacement function for Twig's escape filter. * * Note: This function should be kept in sync with * theme_render_and_autoescape(). * * @param \Twig_Environment $env * A Twig_Environment instance. * @param mixed $arg * The value to be escaped. * @param string $strategy * The escaping strategy. Defaults to 'html'. * @param string $charset * The charset. * @param bool $autoescape * Whether the function is called by the auto-escaping feature (TRUE) or by * the developer (FALSE). * * @return string|null * The escaped, rendered output, or NULL if there is no valid output. * * @todo Refactor this to keep it in sync with theme_render_and_autoescape() * in https://www.drupal.org/node/2575065 */ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $charset = NULL, $autoescape = FALSE) { // Check for a numeric zero int or float. if ($arg === 0 || $arg === 0.0) { return 0; } // Return early for NULL and empty arrays. if ($arg == NULL) { return NULL; } // Keep Twig_Markup objects intact to support autoescaping. if ($autoescape && ($arg instanceof \Twig_Markup || $arg instanceof MarkupInterface)) { return $arg; } $return = NULL; if (is_scalar($arg)) { $return = (string) $arg; } elseif (is_object($arg)) { if ($arg instanceof RenderableInterface) { $arg = $arg->toRenderable(); } elseif (method_exists($arg, '__toString')) { $return = (string) $arg; } elseif (method_exists($arg, 'toString')) { $return = $arg->toString(); } else { throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg)))); } } // We have a string or an object converted to a string: Autoescape it! if (isset($return)) { if ($autoescape && SafeMarkup::isSafe($return, $strategy)) { return $return; } // Drupal only supports the HTML escaping strategy, so provide a // fallback for other strategies. if ($strategy == 'html') { return Html::escape($return); } return twig_escape_filter($env, $return, $strategy, $charset, $autoescape); } // This is a normal render array, which is safe by definition, with // special simple cases already handled. // Early return if this element was pre-rendered (no need to re-render). if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) { return $arg['#markup']; } $arg['#printed'] = FALSE; return $this->renderer->render($arg); }
/** * Tests string formatting with SafeMarkup::format(). * * @dataProvider providerFormat * @covers ::format * * @param string $string * The string to run through SafeMarkup::format(). * @param string $args * The arguments to pass into SafeMarkup::format(). * @param string $expected * The expected result from calling the function. * @param string $message * The message to display as output to the test. * @param bool $expected_is_safe * Whether the result is expected to be safe for HTML display. */ function testFormat($string, $args, $expected, $message, $expected_is_safe) { $result = SafeMarkup::format($string, $args); $this->assertEquals($expected, $result, $message); $this->assertEquals($expected_is_safe, SafeMarkup::isSafe($result), 'SafeMarkup::format correctly sets the result as safe or not safe.'); }