diff --git a/.travis.yml b/.travis.yml index 147eb7df3f3..d70399ca1cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,17 +8,16 @@ cache: - $HOME/.composer/cache/files php: - - 5.3 - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 + - 7.2 - nightly env: - TWIG_EXT=no - - TWIG_EXT=yes before_install: # turn off XDebug @@ -40,10 +39,16 @@ script: | matrix: fast_finish: true - exclude: - - php: 7.0 + include: + - php: 5.3 + dist: precise env: TWIG_EXT=yes - - php: 7.1 + - php: 5.3 + dist: precise + env: TWIG_EXT=no + - php: 5.4 env: TWIG_EXT=yes - - php: nightly + - php: 5.5 + env: TWIG_EXT=yes + - php: 5.6 env: TWIG_EXT=yes diff --git a/CHANGELOG b/CHANGELOG index f738c841ccd..f35edc3c3c0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,28 @@ +* 1.35.4 (2018-07-13) + + * ensured that syntax errors are triggered with the right line + * added the Symfony ctype polyfill as a dependency + * "js" filter now produces valid JSON + +* 1.35.3 (2018-03-20) + + * fixed block names unicity + * fixed counting children of SimpleXMLElement objects + * added missing else clause to avoid infinite loops + * fixed .. (range operator) in sandbox policy + +* 1.35.2 (2018-03-03) + + * fixed a regression in the way the profiler is registered in templates + +* 1.35.1 (2018-03-02) + + * added an exception when using "===" instead of "same as" + * fixed possible array to string conversion concealing actual error + * made variable names deterministic in compiled templates + * fixed length filter when passing an instance of IteratorAggregate + * fixed Environment::resolveTemplate to accept instances of TemplateWrapper + * 1.35.0 (2017-09-27) * added Twig_Profiler_Profile::reset() diff --git a/LICENSE b/LICENSE index b6e17a1a6d9..e401cb971e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2017 by the Twig Team. +Copyright (c) 2009-2018 by the Twig Team. Some rights reserved. diff --git a/README.rst b/README.rst index 81737b0b2c2..f33ea336d86 100644 --- a/README.rst +++ b/README.rst @@ -12,4 +12,4 @@ More Information Read the `documentation`_ for more information. -.. _documentation: http://twig.sensiolabs.org/documentation +.. _documentation: https://twig.symfony.com/documentation diff --git a/composer.json b/composer.json index 09c1ea5e9f7..0a1728f8e9f 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "library", "description": "Twig, the flexible, fast, and secure template language for PHP", "keywords": ["templating"], - "homepage": "http://twig.sensiolabs.org", + "homepage": "https://twig.symfony.com", "license": "BSD-3-Clause", "authors": [ { @@ -14,7 +14,7 @@ }, { "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", + "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" }, { @@ -27,11 +27,12 @@ "forum": "https://groups.google.com/forum/#!forum/twig-users" }, "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "symfony/phpunit-bridge": "~3.3@dev", - "symfony/debug": "~2.7", + "symfony/phpunit-bridge": "^3.3", + "symfony/debug": "^2.7", "psr/container": "^1.0" }, "autoload": { diff --git a/doc/advanced.rst b/doc/advanced.rst index d86db501e52..2715cc57da7 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -543,7 +543,7 @@ to host all the specific tags and filters you want to add to Twig. .. note:: Before writing your own extensions, have a look at the Twig official - extension repository: http://github.com/twigphp/Twig-extensions. + extension repository: https://github.com/twigphp/Twig-extensions. An extension is a class that implements the following interface:: @@ -800,7 +800,7 @@ The simplest way to use methods is to define them on the extension itself:: public function rot13($value) { - return $rot13Provider->rot13($value); + return $this->rot13Provider->rot13($value); } } @@ -849,7 +849,7 @@ It is now possible to move the runtime logic to a new public function rot13($value) { - return $rot13Provider->rot13($value); + return $this->rot13Provider->rot13($value); } } @@ -957,6 +957,6 @@ Testing the node visitors can be complex, so extend your test cases from ``Twig_Test_NodeTestCase``. Examples can be found in the Twig repository `tests/Twig/Node`_ directory. -.. _`rot13`: http://www.php.net/manual/en/function.str-rot13.php +.. _`rot13`: https://secure.php.net/manual/en/function.str-rot13.php .. _`tests/Twig/Fixtures`: https://github.com/twigphp/Twig/tree/master/test/Twig/Tests/Fixtures .. _`tests/Twig/Node`: https://github.com/twigphp/Twig/tree/master/test/Twig/Tests/Node diff --git a/doc/advanced_legacy.rst b/doc/advanced_legacy.rst index 33e9f45a4cc..857fe4fd658 100644 --- a/doc/advanced_legacy.rst +++ b/doc/advanced_legacy.rst @@ -529,7 +529,7 @@ to host all the specific tags and filters you want to add to Twig. .. note:: Before writing your own extensions, have a look at the Twig official - extension repository: http://github.com/twigphp/Twig-extensions. + extension repository: https://github.com/twigphp/Twig-extensions. An extension is a class that implements the following interface:: @@ -879,7 +879,7 @@ Testing the node visitors can be complex, so extend your test cases from ``Twig_Test_NodeTestCase``. Examples can be found in the Twig repository `tests/Twig/Node`_ directory. -.. _`spl_autoload_register()`: http://www.php.net/spl_autoload_register -.. _`rot13`: http://www.php.net/manual/en/function.str-rot13.php +.. _`spl_autoload_register()`: https://secure.php.net/spl_autoload_register +.. _`rot13`: https://secure.php.net/manual/en/function.str-rot13.php .. _`tests/Twig/Fixtures`: https://github.com/twigphp/Twig/tree/master/test/Twig/Tests/Fixtures .. _`tests/Twig/Node`: https://github.com/twigphp/Twig/tree/master/test/Twig/Tests/Node diff --git a/doc/filters/abs.rst b/doc/filters/abs.rst index 22fa59d0369..5c13f49d263 100644 --- a/doc/filters/abs.rst +++ b/doc/filters/abs.rst @@ -15,4 +15,4 @@ The ``abs`` filter returns the absolute value. Internally, Twig uses the PHP `abs`_ function. -.. _`abs`: http://php.net/abs +.. _`abs`: https://secure.php.net/abs diff --git a/doc/filters/convert_encoding.rst b/doc/filters/convert_encoding.rst index f4ebe5807d9..43bf0311854 100644 --- a/doc/filters/convert_encoding.rst +++ b/doc/filters/convert_encoding.rst @@ -24,5 +24,5 @@ Arguments * ``to``: The output charset * ``from``: The input charset -.. _`iconv`: http://php.net/iconv -.. _`mbstring`: http://php.net/mbstring +.. _`iconv`: https://secure.php.net/iconv +.. _`mbstring`: https://secure.php.net/mbstring diff --git a/doc/filters/date.rst b/doc/filters/date.rst index 99a17ab75a6..811fd41bd91 100644 --- a/doc/filters/date.rst +++ b/doc/filters/date.rst @@ -93,8 +93,8 @@ Arguments * ``format``: The date format * ``timezone``: The date timezone -.. _`strtotime`: http://www.php.net/strtotime -.. _`DateTime`: http://www.php.net/DateTime -.. _`DateInterval`: http://www.php.net/DateInterval -.. _`date`: http://www.php.net/date -.. _`DateInterval::format`: http://www.php.net/DateInterval.format +.. _`strtotime`: https://secure.php.net/strtotime +.. _`DateTime`: https://secure.php.net/DateTime +.. _`DateInterval`: https://secure.php.net/DateInterval +.. _`date`: https://secure.php.net/date +.. _`DateInterval::format`: https://secure.php.net/DateInterval.format diff --git a/doc/filters/date_modify.rst b/doc/filters/date_modify.rst index add40b56b01..8a41dd07a72 100644 --- a/doc/filters/date_modify.rst +++ b/doc/filters/date_modify.rst @@ -19,5 +19,5 @@ Arguments * ``modifier``: The modifier -.. _`strtotime`: http://www.php.net/strtotime -.. _`DateTime`: http://www.php.net/DateTime +.. _`strtotime`: https://secure.php.net/strtotime +.. _`DateTime`: https://secure.php.net/DateTime diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index 21491343b6b..6d6f40ef383 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -116,4 +116,4 @@ Arguments * ``strategy``: The escaping strategy * ``charset``: The string charset -.. _`htmlspecialchars`: http://php.net/htmlspecialchars +.. _`htmlspecialchars`: https://secure.php.net/htmlspecialchars diff --git a/doc/filters/first.rst b/doc/filters/first.rst index 674c1f9ed12..da2c2eebb05 100644 --- a/doc/filters/first.rst +++ b/doc/filters/first.rst @@ -22,4 +22,4 @@ a string: It also works with objects implementing the `Traversable`_ interface. -.. _`Traversable`: http://php.net/manual/en/class.traversable.php +.. _`Traversable`: https://secure.php.net/manual/en/class.traversable.php diff --git a/doc/filters/format.rst b/doc/filters/format.rst index f8effd9a940..c600edb5e76 100644 --- a/doc/filters/format.rst +++ b/doc/filters/format.rst @@ -11,6 +11,6 @@ The ``format`` filter formats a given string by replacing the placeholders {# outputs I like foo and bar if the foo parameter equals to the foo string. #} -.. _`sprintf`: http://www.php.net/sprintf +.. _`sprintf`: https://secure.php.net/sprintf .. seealso:: :doc:`replace` diff --git a/doc/filters/json_encode.rst b/doc/filters/json_encode.rst index a39bb476eae..7ff53d29181 100644 --- a/doc/filters/json_encode.rst +++ b/doc/filters/json_encode.rst @@ -17,5 +17,5 @@ Arguments * ``options``: A bitmask of `json_encode options`_ (``{{ data|json_encode(constant('JSON_PRETTY_PRINT')) }}``) -.. _`json_encode`: http://php.net/json_encode -.. _`json_encode options`: http://www.php.net/manual/en/json.constants.php +.. _`json_encode`: https://secure.php.net/json_encode +.. _`json_encode options`: https://secure.php.net/manual/en/json.constants.php diff --git a/doc/filters/last.rst b/doc/filters/last.rst index 345b6573d73..c59ba3fbabc 100644 --- a/doc/filters/last.rst +++ b/doc/filters/last.rst @@ -22,4 +22,4 @@ a string: It also works with objects implementing the `Traversable`_ interface. -.. _`Traversable`: http://php.net/manual/en/class.traversable.php +.. _`Traversable`: https://secure.php.net/manual/en/class.traversable.php diff --git a/doc/filters/length.rst b/doc/filters/length.rst index 5620f3b3c4d..e177b7b69ca 100644 --- a/doc/filters/length.rst +++ b/doc/filters/length.rst @@ -14,6 +14,8 @@ return value of the ``count()`` method. For objects that implement the ``__toString()`` magic method (and not ``Countable``), it will return the length of the string provided by that method. +For objects that implement the ``IteratorAggregate`` interface, ``length`` will use the return value of the ``iterator_count()`` method. + .. code-block:: jinja {% if users|length > 10 %} diff --git a/doc/filters/merge.rst b/doc/filters/merge.rst index 88780dd6ff6..b0571f09daa 100644 --- a/doc/filters/merge.rst +++ b/doc/filters/merge.rst @@ -45,4 +45,4 @@ overridden. Internally, Twig uses the PHP `array_merge`_ function. It supports Traversable objects by transforming those to arrays. -.. _`array_merge`: http://php.net/array_merge +.. _`array_merge`: https://secure.php.net/array_merge diff --git a/doc/filters/number_format.rst b/doc/filters/number_format.rst index f9d6705e72b..4db1ec938fe 100644 --- a/doc/filters/number_format.rst +++ b/doc/filters/number_format.rst @@ -53,4 +53,4 @@ Arguments * ``decimal_point``: The character(s) to use for the decimal point * ``thousand_sep``: The character(s) to use for the thousands separator -.. _`number_format`: http://php.net/number_format +.. _`number_format`: https://secure.php.net/number_format diff --git a/doc/filters/replace.rst b/doc/filters/replace.rst index 8dbb745939b..31392f31154 100644 --- a/doc/filters/replace.rst +++ b/doc/filters/replace.rst @@ -11,6 +11,12 @@ The ``replace`` filter formats a given string by replacing the placeholders {# outputs I like foo and bar if the foo parameter equals to the foo string. #} + {# using % as a delimiter is purely conventional and optional #} + + {{ "I like this and --that--."|replace({'this': foo, '--that--': "bar"}) }} + + {# outputs I like foo and bar #} + Arguments --------- diff --git a/doc/filters/reverse.rst b/doc/filters/reverse.rst index 76fd2c1abd8..8e1d967aa48 100644 --- a/doc/filters/reverse.rst +++ b/doc/filters/reverse.rst @@ -44,4 +44,4 @@ Arguments * ``preserve_keys``: Preserve keys when reversing a mapping or a sequence. -.. _`Traversable`: http://php.net/Traversable +.. _`Traversable`: https://secure.php.net/Traversable diff --git a/doc/filters/slice.rst b/doc/filters/slice.rst index 70bf139e633..defe7a03c89 100644 --- a/doc/filters/slice.rst +++ b/doc/filters/slice.rst @@ -65,7 +65,7 @@ Arguments * ``length``: The size of the slice * ``preserve_keys``: Whether to preserve key or not (when the input is an array) -.. _`Traversable`: http://php.net/manual/en/class.traversable.php -.. _`array_slice`: http://php.net/array_slice -.. _`mb_substr` : http://php.net/mb-substr -.. _`substr`: http://php.net/substr +.. _`Traversable`: https://secure.php.net/manual/en/class.traversable.php +.. _`array_slice`: https://secure.php.net/array_slice +.. _`mb_substr` : https://secure.php.net/mb-substr +.. _`substr`: https://secure.php.net/substr diff --git a/doc/filters/sort.rst b/doc/filters/sort.rst index 350207f8e02..02f9bdfb8fd 100644 --- a/doc/filters/sort.rst +++ b/doc/filters/sort.rst @@ -15,4 +15,4 @@ The ``sort`` filter sorts an array: association. It supports Traversable objects by transforming those to arrays. -.. _`asort`: http://php.net/asort +.. _`asort`: https://secure.php.net/asort diff --git a/doc/filters/split.rst b/doc/filters/split.rst index bbc6d798f28..c69dbe1f6ab 100644 --- a/doc/filters/split.rst +++ b/doc/filters/split.rst @@ -14,13 +14,13 @@ of strings: You can also pass a ``limit`` argument: - * If ``limit`` is positive, the returned array will contain a maximum of - limit elements with the last element containing the rest of string; +* If ``limit`` is positive, the returned array will contain a maximum of + limit elements with the last element containing the rest of string; - * If ``limit`` is negative, all components except the last -limit are - returned; +* If ``limit`` is negative, all components except the last -limit are + returned; - * If ``limit`` is zero, then this is treated as 1. +* If ``limit`` is zero, then this is treated as 1. .. code-block:: jinja @@ -49,5 +49,5 @@ Arguments * ``delimiter``: The delimiter * ``limit``: The limit argument -.. _`explode`: http://php.net/explode -.. _`str_split`: http://php.net/str_split +.. _`explode`: https://secure.php.net/explode +.. _`str_split`: https://secure.php.net/str_split diff --git a/doc/filters/striptags.rst b/doc/filters/striptags.rst index 82953b7b65b..87e0fbf802c 100644 --- a/doc/filters/striptags.rst +++ b/doc/filters/striptags.rst @@ -26,4 +26,4 @@ Arguments * ``allowable_tags``: Tags which should not be stripped -.. _`strip_tags`: http://php.net/strip_tags +.. _`strip_tags`: https://secure.php.net/strip_tags diff --git a/doc/filters/trim.rst b/doc/filters/trim.rst index b598363c272..dfaf4cff4bf 100644 --- a/doc/filters/trim.rst +++ b/doc/filters/trim.rst @@ -40,6 +40,6 @@ Arguments * ``side``: The default is to strip from the left and the right (`both`) sides, but `left` and `right` will strip from either the left side or right side only -.. _`trim`: http://php.net/trim -.. _`ltrim`: http://php.net/ltrim -.. _`rtrim`: http://php.net/rtrim +.. _`trim`: https://secure.php.net/trim +.. _`ltrim`: https://secure.php.net/ltrim +.. _`rtrim`: https://secure.php.net/rtrim diff --git a/doc/filters/url_encode.rst b/doc/filters/url_encode.rst index 5944e59cd5b..4012e5511a5 100644 --- a/doc/filters/url_encode.rst +++ b/doc/filters/url_encode.rst @@ -29,6 +29,6 @@ or an array as query string: that as of Twig 1.16.0, ``urlencode`` **always** uses ``rawurlencode`` (the ``raw`` argument was removed.) -.. _`urlencode`: http://php.net/urlencode -.. _`rawurlencode`: http://php.net/rawurlencode -.. _`http_build_query`: http://php.net/http_build_query +.. _`urlencode`: https://secure.php.net/urlencode +.. _`rawurlencode`: https://secure.php.net/rawurlencode +.. _`http_build_query`: https://secure.php.net/http_build_query diff --git a/doc/functions/block.rst b/doc/functions/block.rst index f5d683c73da..6b8f0d4f435 100644 --- a/doc/functions/block.rst +++ b/doc/functions/block.rst @@ -18,7 +18,7 @@ times, use the ``block`` function: {% block body %}{% endblock %} -The ``block`` function can also be used to display one block of another +The ``block`` function can also be used to display one block from another template: .. code-block:: jinja diff --git a/doc/functions/date.rst b/doc/functions/date.rst index 158dd6a6545..7801ad8a7ec 100644 --- a/doc/functions/date.rst +++ b/doc/functions/date.rst @@ -52,4 +52,4 @@ Arguments * ``date``: The date * ``timezone``: The timezone -.. _`date and time formats`: http://php.net/manual/en/datetime.formats.php +.. _`date and time formats`: https://secure.php.net/manual/en/datetime.formats.php diff --git a/doc/functions/dump.rst b/doc/functions/dump.rst index a231f089e25..611b97901e1 100644 --- a/doc/functions/dump.rst +++ b/doc/functions/dump.rst @@ -65,5 +65,5 @@ Arguments * ``context``: The context to dump -.. _`XDebug`: http://xdebug.org/docs/display -.. _`var_dump`: http://php.net/var_dump +.. _`XDebug`: https://xdebug.org/docs/display +.. _`var_dump`: https://secure.php.net/var_dump diff --git a/doc/functions/random.rst b/doc/functions/random.rst index 168e74f8f7d..57015b840ff 100644 --- a/doc/functions/random.rst +++ b/doc/functions/random.rst @@ -26,4 +26,4 @@ Arguments * ``values``: The values -.. _`mt_rand`: http://php.net/mt_rand +.. _`mt_rand`: https://secure.php.net/mt_rand diff --git a/doc/functions/range.rst b/doc/functions/range.rst index 5c9db08928d..38d282460fc 100644 --- a/doc/functions/range.rst +++ b/doc/functions/range.rst @@ -55,4 +55,4 @@ Arguments * ``high``: The highest possible value of the sequence. * ``step``: The increment between elements of the sequence. -.. _`range`: http://php.net/range +.. _`range`: https://secure.php.net/range diff --git a/doc/installation.rst b/doc/installation.rst index afdcf16593b..860f261a875 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -113,4 +113,4 @@ PHP code but only provides an optimized version of the .. _`download page`: https://github.com/twigphp/Twig/tags .. _`Composer`: https://getcomposer.org/download/ .. _`PHP documentation`: https://wiki.php.net/internals/windows/stepbystepbuild -.. _`Zend Server FAQ`: http://www.zend.com/en/products/server/faq#faqD6 +.. _`Zend Server FAQ`: https://www.zend.com/en/products/server/faq#faqD6 diff --git a/doc/recipes.rst b/doc/recipes.rst index b3ba7f4d7bc..8f46da50eab 100644 --- a/doc/recipes.rst +++ b/doc/recipes.rst @@ -150,7 +150,7 @@ parent's full, unambiguous template path in the extends tag: .. note:: This recipe was inspired by the following Django wiki page: - http://code.djangoproject.com/wiki/ExtendingTemplates + https://code.djangoproject.com/wiki/ExtendingTemplates Customizing the Syntax ---------------------- @@ -565,4 +565,4 @@ include in your templates: 'tag_variable' => array('{[', ']}'), ))); -.. _callback: http://www.php.net/manual/en/function.is-callable.php +.. _callback: https://secure.php.net/manual/en/function.is-callable.php diff --git a/doc/tags/embed.rst b/doc/tags/embed.rst index 66fc21b5fa7..8bca936cf4e 100644 --- a/doc/tags/embed.rst +++ b/doc/tags/embed.rst @@ -88,26 +88,26 @@ two boxes side by side: Without the ``embed`` tag, you have two ways to design your templates: - * Create two "intermediate" base templates that extend the master layout - template: one with vertically stacked boxes to be used by the "foo" and - "bar" pages and another one with side-by-side boxes for the "boom" and - "baz" pages. +* Create two "intermediate" base templates that extend the master layout + template: one with vertically stacked boxes to be used by the "foo" and + "bar" pages and another one with side-by-side boxes for the "boom" and + "baz" pages. - * Embed the markup for the top/bottom and left/right boxes into each page - template directly. +* Embed the markup for the top/bottom and left/right boxes into each page + template directly. These two solutions do not scale well because they each have a major drawback: - * The first solution may indeed work for this simplified example. But imagine - we add a sidebar, which may again contain different, recurring structures - of content. Now we would need to create intermediate base templates for - all occurring combinations of content structure and sidebar structure... - and so on. +* The first solution may indeed work for this simplified example. But imagine + we add a sidebar, which may again contain different, recurring structures + of content. Now we would need to create intermediate base templates for + all occurring combinations of content structure and sidebar structure... + and so on. - * The second solution involves duplication of common code with all its negative - consequences: any change involves finding and editing all affected copies - of the structure, correctness has to be verified for each copy, copies may - go out of sync by careless modifications etc. +* The second solution involves duplication of common code with all its negative + consequences: any change involves finding and editing all affected copies + of the structure, correctness has to be verified for each copy, copies may + go out of sync by careless modifications etc. In such a situation, the ``embed`` tag comes in handy. The common layout code can live in a single base template, and the two different content structures, diff --git a/doc/tags/flush.rst b/doc/tags/flush.rst index 55ef593a921..7fed6150f75 100644 --- a/doc/tags/flush.rst +++ b/doc/tags/flush.rst @@ -14,4 +14,4 @@ The ``flush`` tag tells Twig to flush the output buffer: Internally, Twig uses the PHP `flush`_ function. -.. _`flush`: http://php.net/flush +.. _`flush`: https://secure.php.net/flush diff --git a/doc/tags/if.rst b/doc/tags/if.rst index 12edf980df2..17e2df6f4d3 100644 --- a/doc/tags/if.rst +++ b/doc/tags/if.rst @@ -68,7 +68,10 @@ use more complex ``expressions`` there too: ====================== ==================== empty string false numeric zero false + NAN (Not A Number) true + INF (Infinity) true whitespace-only string true + string "0" or '0' false empty array false null false non-empty array true diff --git a/doc/templates.rst b/doc/templates.rst index 3d60779a09b..bbce1874796 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -313,7 +313,7 @@ will be available in the included template too: The included template ``render_box.html`` is able to access the ``box`` variable. -The filename of the template depends on the template loader. For instance, the +The name of the template depends on the template loader. For instance, the ``Twig_Loader_Filesystem`` allows you to access other templates by giving the filename. You can access templates in subdirectories with a slash: @@ -898,12 +898,12 @@ Extension` chapter. .. _`Twig syntax plugin`: http://plugins.netbeans.org/plugin/37069/php-twig .. _`Twig plugin`: https://github.com/pulse00/Twig-Eclipse-Plugin .. _`Twig language definition`: https://github.com/gabrielcorpse/gedit-twig-template-language -.. _`extension repository`: http://github.com/twigphp/Twig-extensions +.. _`extension repository`: https://github.com/twigphp/Twig-extensions .. _`Twig syntax mode`: https://github.com/bobthecow/Twig-HTML.mode .. _`other Twig syntax mode`: https://github.com/muxx/Twig-HTML.mode .. _`Notepad++ Twig Highlighter`: https://github.com/Banane9/notepadplusplus-twig .. _`web-mode.el`: http://web-mode.org/ -.. _`regular expressions`: http://php.net/manual/en/pcre.pattern.php +.. _`regular expressions`: https://secure.php.net/manual/en/pcre.pattern.php .. _`PHP-twig for atom`: https://github.com/reesef/php-twig -.. _`TwigFiddle`: http://twigfiddle.com/ +.. _`TwigFiddle`: https://twigfiddle.com/ .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack diff --git a/ext/twig/php_twig.h b/ext/twig/php_twig.h index 7fc1601e8c1..e99703f2e68 100644 --- a/ext/twig/php_twig.h +++ b/ext/twig/php_twig.h @@ -15,7 +15,7 @@ #ifndef PHP_TWIG_H #define PHP_TWIG_H -#define PHP_TWIG_VERSION "1.35.0" +#define PHP_TWIG_VERSION "1.35.4-DEV" #include "php.h" diff --git a/ext/twig/twig.c b/ext/twig/twig.c index 6173c006728..b81d8ce3c33 100644 --- a/ext/twig/twig.c +++ b/ext/twig/twig.c @@ -870,6 +870,8 @@ PHP_FUNCTION(twig_template_get_attributes) if (null === $object) { $message = sprintf('Impossible to invoke a method ("%s") on a null variable', $item); + } elseif (is_array($object)) { + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object); } @@ -885,9 +887,9 @@ PHP_FUNCTION(twig_template_get_attributes) type_name = zend_zval_type_name(object); Z_ADDREF_P(object); if (Z_TYPE_P(object) == IS_NULL) { - convert_to_string_ex(&object); - - TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on a %s variable.", item, type_name); + TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on a null variable.", item); + } else if (Z_TYPE_P(object) == IS_ARRAY) { + TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on an array.", item); } else { convert_to_string_ex(&object); diff --git a/lib/Twig/Compiler.php b/lib/Twig/Compiler.php index e90bc987d7a..803eb896250 100644 --- a/lib/Twig/Compiler.php +++ b/lib/Twig/Compiler.php @@ -25,6 +25,7 @@ class Twig_Compiler implements Twig_CompilerInterface protected $sourceOffset; protected $sourceLine; protected $filename; + private $varNameSalt = 0; public function __construct(Twig_Environment $env) { @@ -78,6 +79,7 @@ public function compile(Twig_NodeInterface $node, $indentation = 0) // source code starts at 1 (as we then increment it when we encounter new lines) $this->sourceLine = 1; $this->indentation = $indentation; + $this->varNameSalt = 0; if ($node instanceof Twig_Node_Module) { // to be removed in 2.0 @@ -276,7 +278,7 @@ public function outdent($step = 1) public function getVarName() { - return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->varNameSalt++)); } } diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 5de2f27fd37..9b610f75993 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -16,11 +16,11 @@ */ class Twig_Environment { - const VERSION = '1.35.0'; - const VERSION_ID = 13500; + const VERSION = '1.35.4'; + const VERSION_ID = 13504; const MAJOR_VERSION = 1; const MINOR_VERSION = 35; - const RELEASE_VERSION = 0; + const RELEASE_VERSION = 4; const EXTRA_VERSION = ''; protected $charset; @@ -132,14 +132,14 @@ public function __construct(Twig_LoaderInterface $loader = null, $options = arra // For BC if (is_string($this->originalCache)) { $r = new ReflectionMethod($this, 'writeCacheFile'); - if ($r->getDeclaringClass()->getName() !== __CLASS__) { + if (__CLASS__ !== $r->getDeclaringClass()->getName()) { @trigger_error('The Twig_Environment::writeCacheFile method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED); $this->bcWriteCacheFile = true; } $r = new ReflectionMethod($this, 'getCacheFilename'); - if ($r->getDeclaringClass()->getName() !== __CLASS__) { + if (__CLASS__ !== $r->getDeclaringClass()->getName()) { @trigger_error('The Twig_Environment::getCacheFilename method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED); $this->bcGetCacheFilename = true; @@ -562,12 +562,12 @@ public function isTemplateFresh($name, $time) /** * Tries to load a template consecutively from an array. * - * Similar to loadTemplate() but it also accepts Twig_TemplateInterface instances and an array - * of templates where each is tried to be loaded. + * Similar to loadTemplate() but it also accepts instances of Twig_Template and + * Twig_TemplateWrapper, and an array of templates where each is tried to be loaded. * - * @param string|Twig_Template|array $names A template or an array of templates to try consecutively + * @param string|Twig_Template|Twig_TemplateWrapper|array $names A template or an array of templates to try consecutively * - * @return Twig_Template + * @return Twig_Template|Twig_TemplateWrapper * * @throws Twig_Error_Loader When none of the templates can be found * @throws Twig_Error_Syntax When an error occurred during compilation @@ -583,6 +583,10 @@ public function resolveTemplate($names) return $name; } + if ($name instanceof Twig_TemplateWrapper) { + return $name; + } + try { return $this->loadTemplate($name); } catch (Twig_Error_Loader $e) { diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index 4906f903e9e..fe4a9b4aadd 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -15,8 +15,8 @@ * * This parser implements a "Precedence climbing" algorithm. * - * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm - * @see http://en.wikipedia.org/wiki/Operator-precedence_parser + * @see https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm + * @see https://en.wikipedia.org/wiki/Operator-precedence_parser * * @author Fabien Potencier * @@ -199,11 +199,14 @@ public function parsePrimaryExpression() break; } + // no break default: if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) { $node = $this->parseArrayExpression(); } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) { $node = $this->parseHashExpression(); + } elseif ($token->test(Twig_Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { + throw new Twig_Error_Syntax(sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } else { throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s".', Twig_Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } @@ -313,7 +316,7 @@ public function parsePostfixExpression($node) { while (true) { $token = $this->parser->getCurrentToken(); - if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) { + if (Twig_Token::PUNCTUATION_TYPE == $token->getType()) { if ('.' == $token->getValue() || '[' == $token->getValue()) { $node = $this->parseSubscriptExpression($node); } elseif ('|' == $token->getValue()) { @@ -384,14 +387,14 @@ public function parseSubscriptExpression($node) $lineno = $token->getLine(); $arguments = new Twig_Node_Expression_Array(array(), $lineno); $type = Twig_Template::ANY_CALL; - if ($token->getValue() == '.') { + if ('.' == $token->getValue()) { $token = $stream->next(); if ( - $token->getType() == Twig_Token::NAME_TYPE + Twig_Token::NAME_TYPE == $token->getType() || - $token->getType() == Twig_Token::NUMBER_TYPE + Twig_Token::NUMBER_TYPE == $token->getType() || - ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue())) + (Twig_Token::OPERATOR_TYPE == $token->getType() && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue())) ) { $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno); diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index 34f9ef7d6ea..6fa78e131e1 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -467,7 +467,7 @@ function twig_date_converter(Twig_Environment $env, $date = null, $timezone = nu * * @param string $str String to replace in * @param array|Traversable $from Replace values - * @param string|null $to Replace to, deprecated (@see http://php.net/manual/en/function.strtr.php) + * @param string|null $to Replace to, deprecated (@see https://secure.php.net/manual/en/function.strtr.php) * * @return string */ @@ -661,7 +661,7 @@ function twig_slice(Twig_Environment $env, $item, $start, $length = null, $prese if ($start >= 0 && $length >= 0 && $item instanceof Iterator) { try { - return iterator_to_array(new LimitIterator($item, $start, $length === null ? -1 : $length), $preserveKeys); + return iterator_to_array(new LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys); } catch (OutOfBoundsException $exception) { return array(); } @@ -1000,7 +1000,7 @@ function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', switch ($strategy) { case 'html': - // see http://php.net/htmlspecialchars + // see https://secure.php.net/htmlspecialchars // Using a static variable to avoid initializing the array // each time the function is called. Moving the declaration on the @@ -1040,7 +1040,7 @@ function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', case 'js': // escape all non-alphanumeric characters - // into their \xHH or \uHHHH representations + // into their \x or \uHHHH representations if ('UTF-8' !== $charset) { $string = twig_convert_encoding($string, 'UTF-8', $charset); } @@ -1152,9 +1152,23 @@ function _twig_escape_js_callback($matches) { $char = $matches[0]; - // \xHH - if (!isset($char[1])) { - return '\\x'.strtoupper(substr('00'.bin2hex($char), -2)); + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are ommitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + static $shortMap = array( + '\\' => '\\\\', + '/' => '\\/', + "\x08" => '\b', + "\x0C" => '\f', + "\x0A" => '\n', + "\x0D" => '\r', + "\x09" => '\t', + ); + + if (isset($shortMap[$char])) { + return $shortMap[$char]; } // \uHHHH @@ -1191,8 +1205,8 @@ function _twig_escape_css_callback($matches) /** * This function is adapted from code coming from Zend Framework. * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) - * @license http://framework.zend.com/license/new-bsd New BSD License + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License */ function _twig_escape_html_attr_callback($matches) { @@ -1216,7 +1230,7 @@ function _twig_escape_html_attr_callback($matches) * The following replaces characters undefined in HTML with the * hex entity for the Unicode replacement character. */ - if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) { + if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) { return '�'; } @@ -1224,7 +1238,7 @@ function _twig_escape_html_attr_callback($matches) * Check if the current character to escape has a name entity we should * replace it with while grabbing the hex value of the character. */ - if (strlen($chr) == 1) { + if (1 == strlen($chr)) { $hex = strtoupper(substr('00'.bin2hex($chr), -2)); } else { $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8'); @@ -1263,6 +1277,10 @@ function twig_length_filter(Twig_Environment $env, $thing) return mb_strlen($thing, $env->getCharset()); } + if ($thing instanceof \SimpleXMLElement) { + return count($thing); + } + if (is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) { return mb_strlen((string) $thing, $env->getCharset()); } @@ -1271,6 +1289,10 @@ function twig_length_filter(Twig_Environment $env, $thing) return count($thing); } + if ($thing instanceof \IteratorAggregate) { + return iterator_count($thing); + } + return 1; } @@ -1362,6 +1384,10 @@ function twig_length_filter(Twig_Environment $env, $thing) return strlen($thing); } + if ($thing instanceof \SimpleXMLElement) { + return count($thing); + } + if (is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) { return strlen((string) $thing); } @@ -1370,6 +1396,10 @@ function twig_length_filter(Twig_Environment $env, $thing) return count($thing); } + if ($thing instanceof \IteratorAggregate) { + return iterator_count($thing); + } + return 1; } @@ -1444,7 +1474,7 @@ function twig_test_empty($value) * *
  * {# evaluates to true if the foo variable is an array or a traversable object #}
- * {% if foo is traversable %}
+ * {% if foo is iterable %}
  *     {# ... #}
  * {% endif %}
  * 
diff --git a/lib/Twig/Lexer.php b/lib/Twig/Lexer.php index 85390b28c75..41211eb28b6 100644 --- a/lib/Twig/Lexer.php +++ b/lib/Twig/Lexer.php @@ -235,7 +235,7 @@ protected function lexExpression() $this->moveCursor($match[0]); if ($this->cursor >= $this->end) { - throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', $this->state === self::STATE_BLOCK ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); + throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); } } @@ -337,12 +337,15 @@ protected function lexString() $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { list($expect, $lineno) = array_pop($this->brackets); - if ($this->code[$this->cursor] != '"') { + if ('"' != $this->code[$this->cursor]) { throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } $this->popState(); ++$this->cursor; + } else { + // unlexable + throw new Twig_Error_Syntax(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } } diff --git a/lib/Twig/Loader/Filesystem.php b/lib/Twig/Loader/Filesystem.php index 1275044f8bc..4e8be0d53e5 100644 --- a/lib/Twig/Loader/Filesystem.php +++ b/lib/Twig/Loader/Filesystem.php @@ -175,7 +175,7 @@ public function exists($name) public function isFresh($name, $time) { - return filemtime($this->findTemplate($name)) <= $time; + return filemtime($this->findTemplate($name)) < $time; } protected function findTemplate($name) @@ -279,7 +279,7 @@ private function isAbsolutePath($file) { return strspn($file, '/\\', 0, 1) || (strlen($file) > 3 && ctype_alpha($file[0]) - && substr($file, 1, 1) === ':' + && ':' === substr($file, 1, 1) && strspn($file, '/\\', 2, 1) ) || null !== parse_url($file, PHP_URL_SCHEME) diff --git a/lib/Twig/Node/Expression/Call.php b/lib/Twig/Node/Expression/Call.php index d962b6a501d..ec203484402 100644 --- a/lib/Twig/Node/Expression/Call.php +++ b/lib/Twig/Node/Expression/Call.php @@ -111,7 +111,7 @@ protected function getArguments($callable, $arguments) $named = true; $name = $this->normalizeName($name); } elseif ($named) { - throw new Twig_Error_Syntax(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName)); + throw new Twig_Error_Syntax(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine()); } $parameters[$name] = $node; @@ -143,14 +143,14 @@ protected function getArguments($callable, $arguments) if (array_key_exists($name, $parameters)) { if (array_key_exists($pos, $parameters)) { - throw new Twig_Error_Syntax(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName)); + throw new Twig_Error_Syntax(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine()); } if (count($missingArguments)) { throw new Twig_Error_Syntax(sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $name, $callType, $callName, implode(', ', $names), count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)) - ); + $name, $callType, $callName, implode(', ', $names), count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + ), $this->getTemplateLine()); } $arguments = array_merge($arguments, $optionalArguments); @@ -172,7 +172,7 @@ protected function getArguments($callable, $arguments) $missingArguments[] = $name; } } else { - throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName)); + throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine()); } } @@ -205,7 +205,7 @@ protected function getArguments($callable, $arguments) throw new Twig_Error_Syntax(sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) - ), $unknownParameter ? $unknownParameter->getTemplateLine() : -1); + ), $unknownParameter ? $unknownParameter->getTemplateLine() : $this->getTemplateLine()); } return $arguments; diff --git a/lib/Twig/Node/Expression/Name.php b/lib/Twig/Node/Expression/Name.php index 9d5a21f87d7..7d3d6220e2f 100644 --- a/lib/Twig/Node/Expression/Name.php +++ b/lib/Twig/Node/Expression/Name.php @@ -32,7 +32,12 @@ public function compile(Twig_Compiler $compiler) if ($this->isSpecial()) { $compiler->repr(true); } else { - $compiler->raw('array_key_exists(')->repr($name)->raw(', $context)'); + $compiler + ->raw('(isset($context[') + ->string($name) + ->raw(']) || array_key_exists(') + ->string($name) + ->raw(', $context))'); } } elseif ($this->isSpecial()) { $compiler->raw($this->specialVars[$name]); diff --git a/lib/Twig/NodeTraverser.php b/lib/Twig/NodeTraverser.php index 787629ce344..f00a0bf5c4e 100644 --- a/lib/Twig/NodeTraverser.php +++ b/lib/Twig/NodeTraverser.php @@ -70,8 +70,10 @@ protected function traverseForVisitor(Twig_NodeVisitorInterface $visitor, Twig_N $node = $visitor->enterNode($node, $this->env); foreach ($node as $k => $n) { - if (false !== $n = $this->traverseForVisitor($visitor, $n)) { - $node->setNode($k, $n); + if (false !== $m = $this->traverseForVisitor($visitor, $n)) { + if ($m !== $n) { + $node->setNode($k, $m); + } } else { $node->removeNode($k); } diff --git a/lib/Twig/NodeVisitor/Optimizer.php b/lib/Twig/NodeVisitor/Optimizer.php index 2fa19f3d756..c55e40ff734 100644 --- a/lib/Twig/NodeVisitor/Optimizer.php +++ b/lib/Twig/NodeVisitor/Optimizer.php @@ -56,7 +56,7 @@ protected function doEnterNode(Twig_Node $node, Twig_Environment $env) if (PHP_VERSION_ID < 50400 && self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('Twig_Extension_Sandbox')) { if ($this->inABody) { if (!$node instanceof Twig_Node_Expression) { - if (get_class($node) !== 'Twig_Node') { + if ('Twig_Node' !== get_class($node)) { array_unshift($this->prependedNodes, array()); } } else { @@ -88,7 +88,7 @@ protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) if ($node instanceof Twig_Node_Body) { $this->inABody = false; } elseif ($this->inABody) { - if (!$expression && get_class($node) !== 'Twig_Node' && $prependedNodes = array_shift($this->prependedNodes)) { + if (!$expression && 'Twig_Node' !== get_class($node) && $prependedNodes = array_shift($this->prependedNodes)) { $nodes = array(); foreach (array_unique($prependedNodes) as $name) { $nodes[] = new Twig_Node_SetTemp($name, $node->getTemplateLine()); diff --git a/lib/Twig/NodeVisitor/Sandbox.php b/lib/Twig/NodeVisitor/Sandbox.php index b631b29d4d1..71aa4f029b4 100644 --- a/lib/Twig/NodeVisitor/Sandbox.php +++ b/lib/Twig/NodeVisitor/Sandbox.php @@ -48,6 +48,11 @@ protected function doEnterNode(Twig_Node $node, Twig_Environment $env) $this->functions[$node->getAttribute('name')] = $node; } + // the .. operator is equivalent to the range() function + if ($node instanceof Twig_Node_Expression_Binary_Range && !isset($this->functions['range'])) { + $this->functions['range'] = $node; + } + // wrap print to check __toString() calls if ($node instanceof Twig_Node_Print) { return new Twig_Node_SandboxedPrint($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php index e796f197ac4..6de879a5844 100644 --- a/lib/Twig/Parser.php +++ b/lib/Twig/Parser.php @@ -31,6 +31,7 @@ class Twig_Parser implements Twig_ParserInterface protected $importedSymbols; protected $traits; protected $embeddedTemplates = array(); + private $varNameSalt = 0; public function __construct(Twig_Environment $env) { @@ -49,7 +50,7 @@ public function getEnvironment() public function getVarName() { - return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->stream->getSourceContext()->getCode().$this->varNameSalt++)); } /** @@ -98,6 +99,7 @@ public function parse(Twig_TokenStream $stream, $test = null, $dropNeedle = fals $this->blockStack = array(); $this->importedSymbols = array(array()); $this->embeddedTemplates = array(); + $this->varNameSalt = 0; try { $body = $this->subparse($test, $dropNeedle); @@ -153,7 +155,7 @@ public function subparse($test, $dropNeedle = false) $this->stream->next(); $token = $this->getCurrentToken(); - if ($token->getType() !== Twig_Token::NAME_TYPE) { + if (Twig_Token::NAME_TYPE !== $token->getType()) { throw new Twig_Error_Syntax('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); } @@ -383,7 +385,7 @@ protected function filterBodyNodes(Twig_NodeInterface $node) throw new Twig_Error_Syntax('A template that extends another one cannot start with a byte order mark (BOM); it must be removed.', $node->getTemplateLine(), $this->stream->getSourceContext()); } - throw new Twig_Error_Syntax('A template that extends another one cannot include contents outside Twig blocks. Did you forget to put the contents inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext()); + throw new Twig_Error_Syntax('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext()); } // bypass nodes that will "capture" the output diff --git a/lib/Twig/Profiler/NodeVisitor/Profiler.php b/lib/Twig/Profiler/NodeVisitor/Profiler.php index a395ae7fda1..5db41fea756 100644 --- a/lib/Twig/Profiler/NodeVisitor/Profiler.php +++ b/lib/Twig/Profiler/NodeVisitor/Profiler.php @@ -55,7 +55,7 @@ protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) private function getVarName() { - return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + return sprintf('__internal_%s', hash('sha256', $this->extensionName)); } public function getPriority() diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index 62cb2911369..67a1a5eaedf 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -568,6 +568,8 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ if (null === $object) { $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); + } elseif (is_array($object)) { + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, gettype($object), $object); } @@ -707,7 +709,7 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ } @trigger_error($message, E_USER_DEPRECATED); - return $ret === '' ? '' : new Twig_Markup($ret, $this->env->getCharset()); + return '' === $ret ? '' : new Twig_Markup($ret, $this->env->getCharset()); } return $ret; diff --git a/lib/Twig/Test/IntegrationTestCase.php b/lib/Twig/Test/IntegrationTestCase.php index 016951aa611..382a3f2a205 100644 --- a/lib/Twig/Test/IntegrationTestCase.php +++ b/lib/Twig/Test/IntegrationTestCase.php @@ -132,7 +132,7 @@ public function getLegacyTests() protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs) { if (!$outputs) { - $this->markTestSkipped('no legacy tests to run'); + $this->markTestSkipped('no tests to run'); } if ($condition) { diff --git a/lib/Twig/Token.php b/lib/Twig/Token.php index c7850eccc3d..89066869d20 100644 --- a/lib/Twig/Token.php +++ b/lib/Twig/Token.php @@ -62,7 +62,7 @@ public function __toString() * * type and value (or array of possible values) * * just value (or array of possible values) (NAME_TYPE is used as type) * - * @param array|int $type The type to test + * @param array|string|int $type The type to test * @param array|string|null $values The token value * * @return bool diff --git a/lib/Twig/TokenParser/For.php b/lib/Twig/TokenParser/For.php index 63bf41d6654..8e737c5fafb 100644 --- a/lib/Twig/TokenParser/For.php +++ b/lib/Twig/TokenParser/For.php @@ -40,7 +40,7 @@ public function parse(Twig_Token $token) $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideForFork')); - if ($stream->next()->getValue() == 'else') { + if ('else' == $stream->next()->getValue()) { $stream->expect(Twig_Token::BLOCK_END_TYPE); $else = $this->parser->subparse(array($this, 'decideForEnd'), true); } else { diff --git a/lib/Twig/TokenParser/Sandbox.php b/lib/Twig/TokenParser/Sandbox.php index b8f581cbc3a..7fc70d9a084 100644 --- a/lib/Twig/TokenParser/Sandbox.php +++ b/lib/Twig/TokenParser/Sandbox.php @@ -18,7 +18,7 @@ * {% endsandbox %} * * - * @see http://www.twig-project.org/doc/api.html#sandbox-extension for details + * @see https://twig.symfony.com/doc/api.html#sandbox-extension for details * * @final */ diff --git a/lib/Twig/TokenParser/Use.php b/lib/Twig/TokenParser/Use.php index f15a91ea0df..1ab24e2c6e3 100644 --- a/lib/Twig/TokenParser/Use.php +++ b/lib/Twig/TokenParser/Use.php @@ -21,7 +21,7 @@ * {% block content %}{% endblock %} * * - * @see http://www.twig-project.org/doc/templates.html#horizontal-reuse for details. + * @see https://twig.symfony.com/doc/templates.html#horizontal-reuse for details. * * @final */ diff --git a/lib/Twig/TokenParserBroker.php b/lib/Twig/TokenParserBroker.php index a64013508ec..0d7d6e52a84 100644 --- a/lib/Twig/TokenParserBroker.php +++ b/lib/Twig/TokenParserBroker.php @@ -61,12 +61,12 @@ public function removeTokenParser(Twig_TokenParserInterface $parser) } } - public function addTokenParserBroker(Twig_TokenParserBroker $broker) + public function addTokenParserBroker(self $broker) { $this->brokers[] = $broker; } - public function removeTokenParserBroker(Twig_TokenParserBroker $broker) + public function removeTokenParserBroker(self $broker) { if (false !== $pos = array_search($broker, $this->brokers)) { unset($this->brokers[$pos]); diff --git a/lib/Twig/TokenStream.php b/lib/Twig/TokenStream.php index e1bd7ca1e33..81c043ca9f1 100644 --- a/lib/Twig/TokenStream.php +++ b/lib/Twig/TokenStream.php @@ -139,7 +139,7 @@ public function test($primary, $secondary = null) */ public function isEOF() { - return $this->tokens[$this->current]->getType() === Twig_Token::EOF_TYPE; + return Twig_Token::EOF_TYPE === $this->tokens[$this->current]->getType(); } /** diff --git a/test/Twig/Tests/EnvironmentTest.php b/test/Twig/Tests/EnvironmentTest.php index ca9f2cf8ace..dd8dac9ad6d 100644 --- a/test/Twig/Tests/EnvironmentTest.php +++ b/test/Twig/Tests/EnvironmentTest.php @@ -61,7 +61,7 @@ public function testAutoescapeOption() )); $this->assertEquals('foo<br/ > foo<br/ >', $twig->render('html', array('foo' => 'foo
'))); - $this->assertEquals('foo\x3Cbr\x2F\x20\x3E foo\x3Cbr\x2F\x20\x3E', $twig->render('js', array('bar' => 'foo
'))); + $this->assertEquals('foo\u003Cbr\/\u0020\u003E foo\u003Cbr\/\u0020\u003E', $twig->render('js', array('bar' => 'foo
'))); } public function escapingStrategyCallback($name) diff --git a/test/Twig/Tests/Extension/SandboxTest.php b/test/Twig/Tests/Extension/SandboxTest.php index 9d90e0ea746..e268115c24a 100644 --- a/test/Twig/Tests/Extension/SandboxTest.php +++ b/test/Twig/Tests/Extension/SandboxTest.php @@ -36,6 +36,7 @@ protected function setUp() '1_layout' => '{% block content %}{% endblock %}', '1_child' => "{% extends \"1_layout\" %}\n{% block content %}\n{{ \"a\"|json_encode }}\n{% endblock %}", '1_include' => '{{ include("1_basic1", sandboxed=true) }}', + '1_range_operator' => '{{ (1..2)[0] }}', ); } @@ -143,6 +144,18 @@ public function testSandboxUnallowedFunction() } } + public function testSandboxUnallowedRangeOperator() + { + $twig = $this->getEnvironment(true, array(), self::$templates); + try { + $twig->loadTemplate('1_range_operator')->render(self::$params); + $this->fail('Sandbox throws a SecurityError exception if the unallowed range operator is called'); + } catch (Twig_Sandbox_SecurityError $e) { + $this->assertInstanceOf('Twig_Sandbox_SecurityNotAllowedFunctionError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFunctionError'); + $this->assertEquals('range', $e->getFunctionName(), 'Exception should be raised on the "range" function'); + } + } + public function testSandboxAllowMethodFoo() { $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array('FooObject' => 'foo')); @@ -191,6 +204,12 @@ public function testSandboxAllowFunction() $this->assertEquals('bar', $twig->loadTemplate('1_basic7')->render(self::$params), 'Sandbox allow some functions'); } + public function testSandboxAllowRangeOperator() + { + $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array(), array(), array('range')); + $this->assertEquals('1', $twig->loadTemplate('1_range_operator')->render(self::$params), 'Sandbox allow the range operator'); + } + public function testSandboxAllowFunctionsCaseInsensitive() { foreach (array('getfoobar', 'getFoobar', 'getFooBar') as $name) { @@ -252,7 +271,7 @@ public function testSandboxDisabledAfterIncludeFunctionError() } catch (Throwable $e) { } catch (Exception $e) { } - if ($e === null) { + if (null === $e) { $this->fail('An exception should be thrown for this test to be valid.'); } diff --git a/test/Twig/Tests/Fixtures/autoescape/name.test b/test/Twig/Tests/Fixtures/autoescape/name.test index 04299bed356..98e89399aec 100644 --- a/test/Twig/Tests/Fixtures/autoescape/name.test +++ b/test/Twig/Tests/Fixtures/autoescape/name.test @@ -17,6 +17,6 @@ return array('br' => '
') return array('autoescape' => 'name') --EXPECT-- <br /> -\x3Cbr\x20\x2F\x3E +\u003Cbr\u0020\/\u003E <br />
diff --git a/test/Twig/Tests/Fixtures/exceptions/child_contents_outside_blocks.test b/test/Twig/Tests/Fixtures/exceptions/child_contents_outside_blocks.test index a3f0b50ffc2..2a9c5e7975c 100644 --- a/test/Twig/Tests/Fixtures/exceptions/child_contents_outside_blocks.test +++ b/test/Twig/Tests/Fixtures/exceptions/child_contents_outside_blocks.test @@ -1,5 +1,5 @@ --TEST-- -Exception for child templates defining contents outside blocks defined by parent +Exception for child templates defining content outside blocks defined by parent --TEMPLATE-- {% extends 'base.twig' %} @@ -12,4 +12,4 @@ Content outside a block. {% block sidebar %} {% endblock %} --EXCEPTION-- -Twig_Error_Syntax: A template that extends another one cannot include contents outside Twig blocks. Did you forget to put the contents inside a {% block %} tag in "index.twig" at line 3? +Twig_Error_Syntax: A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag in "index.twig" at line 3? diff --git a/test/Twig/Tests/Fixtures/exceptions/strict_comparison_operator.test b/test/Twig/Tests/Fixtures/exceptions/strict_comparison_operator.test new file mode 100644 index 00000000000..dffbc016ee0 --- /dev/null +++ b/test/Twig/Tests/Fixtures/exceptions/strict_comparison_operator.test @@ -0,0 +1,6 @@ +--TEST-- +The PHP === strict comparison operator is not supported +--TEMPLATE-- +{{ 1 === 2 }} +--EXCEPTION-- +Twig_Error_Syntax: Unexpected operator of value "=". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead in "index.twig" at line 2. diff --git a/test/Twig/Tests/Fixtures/filters/escape_javascript.test b/test/Twig/Tests/Fixtures/filters/escape_javascript.test index 647147a439d..8e7278119a0 100644 --- a/test/Twig/Tests/Fixtures/filters/escape_javascript.test +++ b/test/Twig/Tests/Fixtures/filters/escape_javascript.test @@ -5,4 +5,4 @@ --DATA-- return array() --EXPECT-- -\u00E9\x20\u265C\x20\uD834\uDF06 +\u00E9\u0020\u265C\u0020\uD834\uDF06 diff --git a/test/Twig/Tests/Fixtures/filters/force_escape.test b/test/Twig/Tests/Fixtures/filters/force_escape.test index 85a9b7172bd..eb9cba7cf44 100644 --- a/test/Twig/Tests/Fixtures/filters/force_escape.test +++ b/test/Twig/Tests/Fixtures/filters/force_escape.test @@ -14,5 +14,5 @@ return array() --EXPECT-- foo<br /> -\x20\x20\x20\x20foo\x3Cbr\x20\x2F\x3E\x0A +\u0020\u0020\u0020\u0020foo\u003Cbr\u0020\/\u003E\n foo
diff --git a/test/Twig/Tests/Fixtures/filters/length.test b/test/Twig/Tests/Fixtures/filters/length.test index a7f1e50318c..bad5cf56fe3 100644 --- a/test/Twig/Tests/Fixtures/filters/length.test +++ b/test/Twig/Tests/Fixtures/filters/length.test @@ -6,9 +6,11 @@ {{ number|length }} {{ to_string_able|length }} {{ countable|length }} +{{ iterator_aggregate|length }} {{ null|length }} {{ magic|length }} {{ non_countable|length }} +{{ simple_xml_element|length }} --DATA-- return array( 'array' => array(1, 4), @@ -16,9 +18,11 @@ return array( 'number' => 1000, 'to_string_able' => new ToStringStub('foobar'), 'countable' => new CountableStub(42), /* also asserts we do *not* call __toString() */ + 'iterator_aggregate' => new IteratorAggregateStub(array('a', 'b', 'c')), /* also asserts we do *not* call __toString() */ 'null' => null, 'magic' => new MagicCallStub(), /* used to assert we do *not* call __call */ 'non_countable' => new \StdClass(), + 'simple_xml_element' => new \SimpleXMLElement(''), ); --EXPECT-- 2 @@ -26,6 +30,8 @@ return array( 4 6 42 +3 0 1 1 +2 diff --git a/test/Twig/Tests/Fixtures/regression/block_names_unicity.test b/test/Twig/Tests/Fixtures/regression/block_names_unicity.test new file mode 100644 index 00000000000..40c067391f1 --- /dev/null +++ b/test/Twig/Tests/Fixtures/regression/block_names_unicity.test @@ -0,0 +1,19 @@ +--TEST-- +Block names are unique per template +--TEMPLATE-- +{% extends 'layout' %} +{% block content -%} + {% filter title -%} + second + {% endfilter %} +{% endblock %} +--TEMPLATE(layout)-- +{% filter title -%} + first +{% endfilter %} +{% block content %}{% endblock %} +--DATA-- +return array(); +--EXPECT-- +First +Second diff --git a/test/Twig/Tests/Fixtures/tags/autoescape/functions.test b/test/Twig/Tests/Fixtures/tags/autoescape/functions.test index ce7ea789ebb..653c41b8ef1 100644 --- a/test/Twig/Tests/Fixtures/tags/autoescape/functions.test +++ b/test/Twig/Tests/Fixtures/tags/autoescape/functions.test @@ -80,4 +80,4 @@ unsafe_br()|escape autoescape js safe_br -\x3Cbr\x20\x2F\x3E +\u003Cbr\u0020\/\u003E diff --git a/test/Twig/Tests/Fixtures/tags/autoescape/strategy.legacy.test b/test/Twig/Tests/Fixtures/tags/autoescape/strategy.legacy.test index bbf1356e731..c3f8eddfac8 100644 --- a/test/Twig/Tests/Fixtures/tags/autoescape/strategy.legacy.test +++ b/test/Twig/Tests/Fixtures/tags/autoescape/strategy.legacy.test @@ -7,5 +7,5 @@ --DATA-- return array('var' => '
"') --EXPECT-- -\x3Cbr\x20\x2F\x3E\x22 +\u003Cbr\u0020\/\u003E\u0022 <br />" diff --git a/test/Twig/Tests/Fixtures/tags/autoescape/strategy.test b/test/Twig/Tests/Fixtures/tags/autoescape/strategy.test index e496f60818e..5b69449c24a 100644 --- a/test/Twig/Tests/Fixtures/tags/autoescape/strategy.test +++ b/test/Twig/Tests/Fixtures/tags/autoescape/strategy.test @@ -7,5 +7,5 @@ --DATA-- return array('var' => '
"') --EXPECT-- -\x3Cbr\x20\x2F\x3E\x22 +\u003Cbr\u0020\/\u003E\u0022 <br />" diff --git a/test/Twig/Tests/Fixtures/tags/autoescape/type.test b/test/Twig/Tests/Fixtures/tags/autoescape/type.test index 4f415201db1..1250f0db199 100644 --- a/test/Twig/Tests/Fixtures/tags/autoescape/type.test +++ b/test/Twig/Tests/Fixtures/tags/autoescape/type.test @@ -44,15 +44,15 @@ return array('msg' => "<>\n'\"") 1. autoescape 'html' |escape('js') - + 2. autoescape 'html' |escape('js') - + 3. autoescape 'js' |escape('js') - + 4. no escape @@ -61,9 +61,9 @@ return array('msg' => "<>\n'\"") 5. |escape('js')|escape('html') - + 6. autoescape 'html' |escape('js')|escape('html') - + diff --git a/test/Twig/Tests/IntegrationTest.php b/test/Twig/Tests/IntegrationTest.php index 5d20112de30..9cbb15904b1 100644 --- a/test/Twig/Tests/IntegrationTest.php +++ b/test/Twig/Tests/IntegrationTest.php @@ -307,3 +307,21 @@ public function __toString() throw new Exception('__toString shall not be called on \Countables'); } } + +/** + * This class is used in tests for the length filter. + */ +class IteratorAggregateStub implements \IteratorAggregate +{ + private $data; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function getIterator() + { + return new ArrayIterator($this->data); + } +} diff --git a/test/Twig/Tests/Node/Expression/FilterTest.php b/test/Twig/Tests/Node/Expression/FilterTest.php index 773375c9416..5f34b1d6630 100644 --- a/test/Twig/Tests/Node/Expression/FilterTest.php +++ b/test/Twig/Tests/Node/Expression/FilterTest.php @@ -118,7 +118,7 @@ public function testCompileWithWrongNamedArgumentName() /** * @expectedException Twig_Error_Syntax - * @expectedExceptionMessage Value for argument "from" is required for filter "replace". + * @expectedExceptionMessage Value for argument "from" is required for filter "replace" at line 1. */ public function testCompileWithMissingNamedArgument() { diff --git a/test/Twig/Tests/Node/Expression/NullCoalesceTest.php b/test/Twig/Tests/Node/Expression/NullCoalesceTest.php index a37490baf29..e179a9fd916 100644 --- a/test/Twig/Tests/Node/Expression/NullCoalesceTest.php +++ b/test/Twig/Tests/Node/Expression/NullCoalesceTest.php @@ -21,9 +21,9 @@ public function getTests() if (PHP_VERSION_ID >= 70000) { $tests[] = array($node, "((// line 1\n\$context[\"foo\"]) ?? (2))"); } elseif (PHP_VERSION_ID >= 50400) { - $tests[] = array($node, "(((// line 1\narray_key_exists(\"foo\", \$context) && !(null === (isset(\$context[\"foo\"]) ? \$context[\"foo\"] : null)))) ? ((isset(\$context[\"foo\"]) ? \$context[\"foo\"] : null)) : (2))"); + $tests[] = array($node, "(((// line 1\n(isset(\$context[\"foo\"]) || array_key_exists(\"foo\", \$context)) && !(null === (isset(\$context[\"foo\"]) ? \$context[\"foo\"] : null)))) ? ((isset(\$context[\"foo\"]) ? \$context[\"foo\"] : null)) : (2))"); } else { - $tests[] = array($node, "(((// line 1\narray_key_exists(\"foo\", \$context) && !(null === \$this->getContext(\$context, \"foo\")))) ? (\$this->getContext(\$context, \"foo\")) : (2))"); + $tests[] = array($node, "(((// line 1\n(isset(\$context[\"foo\"]) || array_key_exists(\"foo\", \$context)) && !(null === \$this->getContext(\$context, \"foo\")))) ? (\$this->getContext(\$context, \"foo\")) : (2))"); } return $tests; diff --git a/test/Twig/Tests/TemplateTest.php b/test/Twig/Tests/TemplateTest.php index c22a433361e..336fc1f56e7 100644 --- a/test/Twig/Tests/TemplateTest.php +++ b/test/Twig/Tests/TemplateTest.php @@ -58,6 +58,7 @@ public function getAttributeExceptions() array('{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1.'), array('{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1.'), array('{{ null.a() }}', 'Impossible to invoke a method ("a") on a null variable in "%s" at line 1.'), + array('{{ array.a() }}', 'Impossible to invoke a method ("a") on an array in "%s" at line 1.'), array('{{ empty_array.a }}', 'Key "a" does not exist as the array is empty in "%s" at line 1.'), array('{{ array.a }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1.'), array('{{ attribute(array, -10) }}', 'Key "-10" for array with keys "foo" does not exist in "%s" at line 1.'), diff --git a/test/Twig/Tests/escapingTest.php b/test/Twig/Tests/escapingTest.php index 9b98dddcf13..80a108ab343 100644 --- a/test/Twig/Tests/escapingTest.php +++ b/test/Twig/Tests/escapingTest.php @@ -3,8 +3,8 @@ /** * This class is adapted from code coming from Zend Framework. * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) - * @license http://framework.zend.com/license/new-bsd New BSD License + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License */ class Twig_Test_EscapingTest extends \PHPUnit\Framework\TestCase { @@ -51,13 +51,15 @@ class Twig_Test_EscapingTest extends \PHPUnit\Framework\TestCase protected $jsSpecialChars = array( /* HTML special chars - escape without exception to hex */ - '<' => '\\x3C', - '>' => '\\x3E', - '\'' => '\\x27', - '"' => '\\x22', - '&' => '\\x26', + '<' => '\\u003C', + '>' => '\\u003E', + '\'' => '\\u0027', + '"' => '\\u0022', + '&' => '\\u0026', + '/' => '\\/', /* Characters beyond ASCII value 255 to unicode escape */ 'Ā' => '\\u0100', + '😀' => '\\uD83D\\uDE00', /* Immune chars excluded */ ',' => ',', '.' => '.', @@ -70,12 +72,14 @@ class Twig_Test_EscapingTest extends \PHPUnit\Framework\TestCase '0' => '0', '9' => '9', /* Basic control characters and null */ - "\r" => '\\x0D', - "\n" => '\\x0A', - "\t" => '\\x09', - "\0" => '\\x00', + "\r" => '\r', + "\n" => '\n', + "\x08" => '\b', + "\t" => '\t', + "\x0C" => '\f', + "\0" => '\\u0000', /* Encode spaces for quoteless attribute protection */ - ' ' => '\\x20', + ' ' => '\\u0020', ); protected $urlSpecialChars = array(