diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 248c795..89cdc53 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1697,6 +1697,25 @@ function template_preprocess_table(&$variables) { } /** + * Prepares variables for tablesort indicator templates. + * + * Default template: tablesort-indicator.html.twig. + * + * @param array $variables + * An associative array containing: + * - style: Set to either 'asc' or 'desc'. This determines which icon to show. + */ +function template_preprocess_tablesort_indicator(&$variables) { + // Provide the image attributes for an ascending or descending image. + if ($variables['style'] == 'asc') { + $variables['arrow_asc'] = file_create_url('core/misc/arrow-asc.png'); + } + else { + $variables['arrow_desc'] = file_create_url('core/misc/arrow-desc.png'); + } +} + +/** * Prepares variables for item list templates. * * Default template: item-list.html.twig. diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 42c7dea..34ad258 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -84,6 +84,9 @@ public static function registerTwig(ContainerBuilder $container) { ->addArgument(DRUPAL_ROOT); $container->setAlias('twig.loader', 'twig.loader.filesystem'); + $twig_extension = new Definition('Drupal\Core\Template\TwigExtension'); + $twig_extension->addMethodCall('setGenerators', array(new Reference('url_generator'))); + $container->register('twig', 'Drupal\Core\Template\TwigEnvironment') ->addArgument(new Reference('twig.loader')) ->addArgument(array( @@ -99,7 +102,7 @@ public static function registerTwig(ContainerBuilder $container) { )) ->addArgument(new Reference('module_handler')) ->addArgument(new Reference('theme_handler')) - ->addMethodCall('addExtension', array(new Definition('Drupal\Core\Template\TwigExtension'))) + ->addMethodCall('addExtension', array($twig_extension)) // @todo Figure out what to do about debugging functions. // @see http://drupal.org/node/1804998 ->addMethodCall('addExtension', array(new Definition('Twig_Extension_Debug'))) diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 69e0bb3..05c7390 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -12,6 +12,8 @@ namespace Drupal\Core\Template; +use Drupal\Core\Routing\UrlGeneratorInterface; + /** * A class providing Drupal Twig extensions. * @@ -22,13 +24,35 @@ class TwigExtension extends \Twig_Extension { /** + * The URL generator. + * + * @var \Drupal\Core\Routing\UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * Constructs \Drupal\Core\Template\TwigExtension. + * + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The URL generator. + */ + public function setGenerators(UrlGeneratorInterface $url_generator) { + $this->urlGenerator = $url_generator; + return $this; + } + + /** * {@inheritdoc} */ public function getFunctions() { return array( - new \Twig_SimpleFunction('url', 'url'), // This function will receive a renderable array, if an array is detected. new \Twig_SimpleFunction('render_var', 'twig_render_var'), + // The url and path function are defined in close parallel to those found + // in \Symfony\Bridge\Twig\Extension\RoutingExtension + new \Twig_SimpleFunction('url', array($this, 'getUrl'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), + new \Twig_SimpleFunction('path', array($this, 'getPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), + new \Twig_SimpleFunction('url_from_path', array($this, 'getUrlFromPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), ); } @@ -88,4 +112,101 @@ public function getName() { return 'drupal_core'; } + /** + * Generates a URL path given a route name and parameters. + * + * @param $name + * The name of the route. + * @param array $parameters + * An associative array of route parameters names and values. + * @param array $options + * (optional) An associative array of additional options. The 'absolute' + * option is forced to be FALSE. + * @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute(). + * + * @return string + * The generated URL path (relative URL) for the given route. + */ + public function getPath($name, $parameters = array(), $options = array()) { + $options['absolute'] = FALSE; + return $this->urlGenerator->generateFromRoute($name, $parameters, $options); + } + + /** + * Generates an absolute URL given a route name and parameters. + * + * @param $name + * The name of the route. + * @param array $parameters + * An associative array of route parameter names and values. + * @param array $options + * (optional) An associative array of additional options. The 'absolute' + * option is forced to be TRUE. + * + * @return string + * The generated absolute URL for the given route. + * + * @todo Add an option for scheme-relative URLs. + */ + public function getUrl($name, $parameters = array(), $options = array()) { + $options['absolute'] = TRUE; + return $this->urlGenerator->generateFromRoute($name, $parameters, $options); + } + + /** + * Generates an absolute URL given a path. + * + * @param string $path + * The path. + * @param array $options + * (optional) An associative array of additional options. The 'absolute' + * option is forced to be TRUE. + * + * @return string + * The generated absolute URL for the given path. + */ + public function getUrlFromPath($path, $options = array()) { + $options['absolute'] = TRUE; + return $this->urlGenerator->generateFromPath($path, $options); + } + + /** + * Determines at compile time whether the generated URL will be safe. + * + * Saves the unneeded automatic escaping for performance reasons. + * + * The URL generation process percent encodes non-alphanumeric characters. + * Thus, the only character within an URL that must be escaped in HTML is the + * ampersand ("&") which separates query params. Thus we cannot mark + * the generated URL as always safe, but only when we are sure there won't be + * multiple query params. This is the case when there are none or only one + * constant parameter given. E.g. we know beforehand this will not need to + * be escaped: + * - path('route') + * - path('route', {'param': 'value'}) + * But the following may need to be escaped: + * - path('route', var) + * - path('route', {'param': ['val1', 'val2'] }) // a sub-array + * - path('route', {'param1': 'value1', 'param2': 'value2'}) + * If param1 and param2 reference placeholders in the route, it would not + * need to be escaped, but we don't know that in advance. + * + * @param \Twig_Node $args_node + * The arguments of the path/url functions. + * + * @return array + * An array with the contexts the URL is safe + */ + public function isUrlGenerationSafe(\Twig_Node $args_node) { + // Support named arguments. + $parameter_node = $args_node->hasNode('parameters') ? $args_node->getNode('parameters') : ($args_node->hasNode(1) ? $args_node->getNode(1) : NULL); + + if (!isset($parameter_node) || $parameter_node instanceof \Twig_Node_Expression_Array && count($parameter_node) <= 2 && + (!$parameter_node->hasNode(1) || $parameter_node->getNode(1) instanceof \Twig_Node_Expression_Constant)) { + return array('html'); + } + + return array(); + } + } diff --git a/core/modules/node/templates/node-add-list.html.twig b/core/modules/node/templates/node-add-list.html.twig index 7323896..258d3e6 100644 --- a/core/modules/node/templates/node-add-list.html.twig +++ b/core/modules/node/templates/node-add-list.html.twig @@ -24,7 +24,7 @@ {% else %}

- {% set create_content = url('admin/structure/types/add') %} + {% set create_content = path('node.type_add') %} {% trans %} You have not created any content types yet. Go to the content type creation page to add a new content type. {% endtrans %} diff --git a/core/modules/system/src/Tests/Theme/EngineTwigTest.php b/core/modules/system/src/Tests/Theme/EngineTwigTest.php index 2e89f47..0f69878 100644 --- a/core/modules/system/src/Tests/Theme/EngineTwigTest.php +++ b/core/modules/system/src/Tests/Theme/EngineTwigTest.php @@ -41,4 +41,26 @@ function testTwigVariableDataTypes() { } } + /** + * Tests the url and url_generate Twig functions. + */ + public function testTwigUrlGenerator() { + $this->drupalGet('twig-theme-test/url-generator'); + // Find the absolute URL of the current site. + $url_generator = $this->container->get('url_generator'); + $expected = array( + 'path (as route) not absolute: ' . $url_generator->generateFromRoute('user.register'), + 'url (as route) absolute: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE)), + 'path (as route) not absolute with fragment: ' . $url_generator->generateFromRoute('user.register', array(), array('fragment' => 'bottom')), + 'url (as route) absolute despite option: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE)), + 'url (as route) absolute with fragment: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE, 'fragment' => 'bottom')), + ); + // Make sure we got something. + $content = $this->drupalGetContent(); + $this->assertFalse(empty($content), 'Page content is not empty'); + foreach ($expected as $string) { + $this->assertRaw('

' . $string . '
'); + } + } + } diff --git a/core/modules/system/templates/tablesort-indicator.html.twig b/core/modules/system/templates/tablesort-indicator.html.twig index 1f0eb9d..059805f 100644 --- a/core/modules/system/templates/tablesort-indicator.html.twig +++ b/core/modules/system/templates/tablesort-indicator.html.twig @@ -6,11 +6,13 @@ * Available variables: * - style: Either 'asc' or 'desc', indicating the sorting direction. * + * @see template_preprocess_tablesort_indicator() + * * @ingroup themeable */ #} {% if style == 'asc' -%} - {{ 'sort ascending'|t }} + {{ 'sort ascending'|t }} {% else -%} - {{ 'sort descending'|t }} + {{ 'sort descending'|t }} {% endif %} diff --git a/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php b/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php index 45654ea..d454e5f 100644 --- a/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php +++ b/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php @@ -28,4 +28,13 @@ public function transBlockRender() { ); } + /** + * Renders for testing url_generator functions in a Twig template. + */ + public function urlGeneratorRender() { + return array( + '#theme' => 'twig_theme_test_url_generator', + ); + } + } diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.url_generator.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.url_generator.html.twig new file mode 100644 index 0000000..131a612 --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.url_generator.html.twig @@ -0,0 +1,7 @@ +{# Test the url and path twig functions #} +
path (as route) not absolute: {{ path('user.register') }}
+
url (as route) absolute: {{ url('user.register') }}
+ +
path (as route) not absolute with fragment: {{ path('user.register', {}, {'fragment': 'bottom' }) }}
+
url (as route) absolute despite option: {{ url('user.register', {}, {'absolute': false }) }}
+
url (as route) absolute with fragment: {{ url('user.register', {}, {'fragment': 'bottom' }) }}
diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module index 5874cca..0497c7b 100644 --- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module +++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module @@ -23,6 +23,10 @@ function twig_theme_test_theme($existing, $type, $theme, $path) { 'variables' => array('script' => ''), 'template' => 'twig-raw-test', ); + $items['twig_theme_test_url_generator'] = array( + 'variables' => array(), + 'template' => 'twig_theme_test.url_generator', + ); return $items; } diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml index 96befd4..75b9bbb 100644 --- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml +++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.routing.yml @@ -11,3 +11,10 @@ twig_theme_test.trans: _content: '\Drupal\twig_theme_test\TwigThemeTestController::transBlockRender' requirements: _access: 'TRUE' + +twig_theme_test_url_generator: + path: '/twig-theme-test/url-generator' + defaults: + _content: '\Drupal\twig_theme_test\TwigThemeTestController::urlGeneratorRender' + requirements: + _access: 'TRUE' diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 54bef82..2ceb7d0 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -336,6 +336,7 @@ function simpletest_script_init() { } $_SERVER['HTTP_HOST'] = $host; + $_SERVER['SERVER_PORT'] = isset($parsed_url['port']) ? $parsed_url['port'] : 80; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['SERVER_ADDR'] = '127.0.0.1'; $_SERVER['SERVER_PORT'] = $port; diff --git a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php new file mode 100644 index 0000000..d528ee5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php @@ -0,0 +1,75 @@ + TRUE, + 'cache' => FALSE, + 'autoescape' => TRUE, + 'optimizations' => 0 + )); + $twig->addExtension((new TwigExtension())->setGenerators($this->getMock('Drupal\Core\Routing\UrlGeneratorInterface'))); + + $nodes = $twig->parse($twig->tokenize($template)); + + $this->assertSame($expected, $nodes->getNode('body') + ->getNode(0) + ->getNode('expr') instanceof \Twig_Node_Expression_Filter); + } + + /** + * Provides tests data for testEscaping + * + * @return array + * An array of test data each containing of a twig template string and + * a boolean expecting whether the path will be safe. + */ + public function providerTestEscaping() { + return array( + array('{{ path("foo") }}', FALSE), + array('{{ path("foo", {}) }}', FALSE), + array('{{ path("foo", { foo: "foo" }) }}', FALSE), + array('{{ path("foo", foo) }}', TRUE), + array('{{ path("foo", { foo: foo }) }}', TRUE), + array('{{ path("foo", { foo: ["foo", "bar"] }) }}', TRUE), + array('{{ path("foo", { foo: "foo", bar: "bar" }) }}', TRUE), + array('{{ path(name = "foo", parameters = {}) }}', FALSE), + array('{{ path(name = "foo", parameters = { foo: "foo" }) }}', FALSE), + array('{{ path(name = "foo", parameters = foo) }}', TRUE), + array( + '{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}', + TRUE + ), + array('{{ path(name = "foo", parameters = { foo: foo }) }}', TRUE), + array( + '{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}', + TRUE + ), + ); + } + +} diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme index abf6c4f..8cdc9c1 100644 --- a/core/themes/seven/seven.theme +++ b/core/themes/seven/seven.theme @@ -120,9 +120,6 @@ function seven_preprocess_node_add_list(&$variables) { $variables['types'][$type->type]['url'] = \Drupal::url('node.add', array('node_type' => $type->type)); } } - else { - $variables['add_content_type_url'] = \Drupal::url('node.type_add'); - } } /** diff --git a/core/themes/seven/templates/node-add-list.html.twig b/core/themes/seven/templates/node-add-list.html.twig index 4d5e020..897dd1e 100644 --- a/core/themes/seven/templates/node-add-list.html.twig +++ b/core/themes/seven/templates/node-add-list.html.twig @@ -8,7 +8,6 @@ * - url: Path to the add content of this type page. * - label: The title of this type of content. * - description: Description of this type of content. - * - add_content_type_url: Path to the add content type page. * * @see template_preprocess_node_add_list() * @see seven_preprocess_node_add_list() @@ -22,8 +21,9 @@ {% else %}

+ {% set create_content = path('node.type_add') %} {% trans %} - You have not created any content types yet. Go to the content type creation page to add a new content type. + You have not created any content types yet. Go to the content type creation page to add a new content type. {% endtrans %}

{% endif %}