diff --git a/composer.json b/composer.json index 7ce3db6abc410f2856951fcf42bbeba761d6c768..d6cdc7fea22db5f4c2d2121c277196a27ee8c782 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "symfony/http-foundation": "2.4.*", "symfony/http-kernel": "2.4.*", "symfony/routing": "2.4.*", - "symfony/serializer": "2.4.*", + "symfony/serializer": "2.5.*", "symfony/validator": "2.4.*", "symfony/yaml": "dev-master#e49a47d60348665261f6e279ba383241deb73cab", "twig/twig": "1.15.*", diff --git a/composer.lock b/composer.lock index fdc46fe565b4488113776c1ec05bc6c1464e662c..e1f9771ee0bfe99fe4da622932aaea84ac032e6e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e1a7eeed7d09f5c76f642f07b7a0a579", + "hash": "2b2309c748ec92230880579aaedfa1e6", "packages": [ { "name": "doctrine/annotations", @@ -2027,17 +2027,17 @@ }, { "name": "symfony/serializer", - "version": "v2.4.1", + "version": "v2.5.2", "target-dir": "Symfony/Component/Serializer", "source": { "type": "git", "url": "https://github.com/symfony/Serializer.git", - "reference": "60c54346958604379392672a3a998650a169a7f4" + "reference": "33185b290310ab1fd8283fb8ed2a434cdb88b9b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Serializer/zipball/60c54346958604379392672a3a998650a169a7f4", - "reference": "60c54346958604379392672a3a998650a169a7f4", + "url": "https://api.github.com/repos/symfony/Serializer/zipball/33185b290310ab1fd8283fb8ed2a434cdb88b9b9", + "reference": "33185b290310ab1fd8283fb8ed2a434cdb88b9b9", "shasum": "" }, "require": { @@ -2046,7 +2046,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -2061,7 +2061,9 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" }, { "name": "Symfony Community", @@ -2070,7 +2072,7 @@ ], "description": "Symfony Serializer Component", "homepage": "http://symfony.com", - "time": "2014-01-01 08:14:50" + "time": "2014-07-09 09:05:48" }, { "name": "symfony/translation", @@ -2423,12 +2425,8 @@ "time": "2013-06-12 19:46:58" } ], - "packages-dev": [ - - ], - "aliases": [ - - ], + "packages-dev": [], + "aliases": [], "minimum-stability": "stable", "stability-flags": { "symfony/yaml": 20, @@ -2438,10 +2436,9 @@ "symfony-cmf/routing": 15, "phpunit/phpunit-mock-objects": 20 }, + "prefer-stable": false, "platform": { "php": ">=5.4.2" }, - "platform-dev": [ - - ] + "platform-dev": [] } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 248c7954eeb7919212b457c21be329e2ddbb5459..89cdc53243d87d514e0d121414c24aabef4eb9ba 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1696,6 +1696,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. * diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 42c7deaeb6c66a339356ba51f1f46eb20b2f8042..34ad2589207bb5d874c31216c386c2cd97324a9a 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 69e0bb30465b0b97d718d0dfcbd22314c36ac4e4..05c739035cbcf1f83e383c24b710b4000ae7541b 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. * @@ -21,14 +23,36 @@ */ 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 73238969e3febe13a609965c8ff51b5c8bfac343..258d3e631f73766882e5d3d644c8c2796b3677f0 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 @@ </dl> {% else %} <p> - {% 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 <a href="{{ create_content }}">content type creation page</a> 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 2e89f47d2ad9f5bd9172c4cbbe42c174995f8b32..0f69878f3cc03e87d08affe45654858ab2f3330e 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('<div>' . $string . '</div>'); + } + } + } diff --git a/core/modules/system/templates/tablesort-indicator.html.twig b/core/modules/system/templates/tablesort-indicator.html.twig index 1f0eb9d2a6d804c3f75726545309be0047e2cd4a..059805f1d41a285f49e18e803dd267248e9e258d 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' -%} - <img src="{{ url('core/misc/arrow-asc.png') }}" width="13" height="13" alt="{{ 'sort ascending'|t }}" title="{{ 'sort ascending'|t }}" /> + <img src="{{ arrow_asc }}" width="13" height="13" alt="{{ 'sort ascending'|t }}" title="{{ 'sort ascending'|t }}" /> {% else -%} - <img src="{{ url('core/misc/arrow-desc.png') }}" width="13" height="13" alt="{{ 'sort descending'|t }}" title="{{ 'sort descending'|t }}" /> + <img src="{{ arrow_desc }}" width="13" height="13" alt="{{ 'sort descending'|t }}" title="{{ '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 45654ea206dc8bfe27bf098ca434faccc62a886a..d454e5f30624ffc02a906f54b6fd39eeab965013 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 0000000000000000000000000000000000000000..131a6120ba402d16cb41eb75f16c33d499b81bfd --- /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 #} +<div>path (as route) not absolute: {{ path('user.register') }}</div> +<div>url (as route) absolute: {{ url('user.register') }}</div> + +<div>path (as route) not absolute with fragment: {{ path('user.register', {}, {'fragment': 'bottom' }) }}</div> +<div>url (as route) absolute despite option: {{ url('user.register', {}, {'absolute': false }) }}</div> +<div>url (as route) absolute with fragment: {{ url('user.register', {}, {'fragment': 'bottom' }) }}</div> 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 5874cca75482d8dcbee0012b19b8f2cafbea5e94..0497c7b9b802d812e28dd82b5710043478a21569 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 96befd49a891d840f917bedff8bc13e23d6fd39d..75b9bbb11fbfaf7ff0336bb99ef9a209b4f875c9 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/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d528ee544539ea5607288866f8f1bffbfb4ab372 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php @@ -0,0 +1,75 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Template\TwigExtensionTest. + */ + +namespace Drupal\Tests\Core\Template; + +use Drupal\Core\Template\TwigExtension; +use Drupal\Tests\UnitTestCase; + +/** + * Tests the twig extension. + * + * @group Template + * + * @coversDefaultClass \Drupal\Core\Template\TwigExtension + */ +class TwigExtensionTest extends UnitTestCase { + + /** + * Tests the escaping + * @dataProvider providerTestEscaping + * + * @covers + */ + public function testEscaping($template, $expected) { + $twig = new \Twig_Environment(NULL, array( + 'debug' => 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 abf6c4f6dbbf11164b3fd02155c4f76e3a6c471a..8cdc9c1725e1b8e992235564aebeddf4a8a4948c 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 4d5e020687cb54585d1b0bfea84540358807b788..897dd1ea13c3425a1bde7c52445b0d559e19fc76 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 @@ </ul> {% else %} <p> + {% set create_content = path('node.type_add') %} {% trans %} - You have not created any content types yet. Go to the <a href="{{ add_content_type_url }}">content type creation page</a> to add a new content type. + You have not created any content types yet. Go to the <a href="{{ create_content }}">content type creation page</a> to add a new content type. {% endtrans %} </p> {% endif %}