Merge branch '3.x' into 4.x

* 3.x:
  Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
  add docs & links
This commit is contained in:
Fabien Potencier
2023-12-19 08:20:44 +01:00
11 changed files with 177 additions and 24 deletions
+42 -9
View File
@@ -12,8 +12,8 @@ missing items:
<table>
{% for row in items|batch(3, 'No item') %}
<tr>
{% for column in row %}
<td>{{ column }}</td>
{% for index, column in row %}
<td>{{ index }} - {{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
@@ -25,14 +25,47 @@ The above example will be rendered as:
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
<td>0 - a</td>
<td>1 - b</td>
<td>2 - c</td>
</tr>
<tr>
<td>d</td>
<td>No item</td>
<td>No item</td>
<td>3 - d</td>
<td>4 - No item</td>
<td>5 - No item</td>
</tr>
</table>
If you choose to set the third parameter ``preserve_keys`` to ``false``, the keys will be reset in each loop.
.. code-block:: html+twig
{% set items = ['a', 'b', 'c', 'd'] %}
<table>
{% for row in items|batch(3, 'No item', false) %}
<tr>
{% for index, column in row %}
<td>{{ index }} - {{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
The above example will be rendered as:
.. code-block:: html+twig
<table>
<tr>
<td>0 - a</td>
<td>1 - b</td>
<td>2 - c</td>
</tr>
<tr>
<td>0 - d</td>
<td>1 - No item</td>
<td>2 - No item</td>
</tr>
</table>
@@ -41,4 +74,4 @@ Arguments
* ``size``: The size of the batch; fractional numbers will be rounded up
* ``fill``: Used to fill in missing items
* ``preserve_keys``: Whether to preserve keys or not
* ``preserve_keys``: Whether to preserve keys or not (defaults to ``true``)
+3 -2
View File
@@ -47,7 +47,7 @@ Timezone
By default, the date is displayed by applying the default timezone (the one
specified in php.ini or declared in Twig -- see below), but you can override
it by explicitly specifying a timezone:
it by explicitly specifying a supported `timezone`_:
.. code-block:: twig
@@ -68,7 +68,7 @@ The default timezone can also be set globally by calling ``setTimezone()``::
Arguments
---------
* ``format``: The date format
* ``format``: The date format (default format is ``F j, Y H:i``, which will render as ``January 11, 2024 15:17``)
* ``timezone``: The date timezone
.. _`strtotime`: https://www.php.net/strtotime
@@ -76,3 +76,4 @@ Arguments
.. _`DateInterval`: https://www.php.net/DateInterval
.. _`date`: https://www.php.net/date
.. _`DateInterval::format`: https://www.php.net/DateInterval.format
.. _`timezone`: https://www.php.net/manual/en/timezones.php
+6
View File
@@ -9,3 +9,9 @@ iterate over the keys of an array:
{% for key in array|keys %}
...
{% endfor %}
.. note::
Internally, Twig uses the PHP `array_keys`_ function.
.. _`array_keys`: https://www.php.net/array_keys
+6
View File
@@ -8,3 +8,9 @@ The ``lower`` filter converts a value to lowercase:
{{ 'WELCOME'|lower }}
{# outputs 'welcome' #}
.. note::
Internally, Twig uses the PHP `mb_strtolower`_ function.
.. _`mb_strtolower`: https://www.php.net/manual/fr/function.mb-strtolower.php
+6
View File
@@ -27,3 +27,9 @@ Arguments
* ``arrow``: The arrow function
* ``initial``: The initial value
.. note::
Internally, Twig uses the PHP `array_reduce`_ function.
.. _`array_reduce`: https://www.php.net/array_reduce
+23 -1
View File
@@ -54,6 +54,28 @@ negative then the sequence will stop that many elements from the end of the
variable. If it is omitted, then the sequence will have everything from offset
up until the end of the variable.
The argument ``preserve_keys`` is used to reset the index during the loop.
.. code-block:: twig
{% for key, value in [1, 2, 3, 4, 5]|slice(1, 2, true) %}
{{ key }} - {{ value }}
{% endfor %}
{# output
1 - 2
2 - 3
#}
{% for key, value in [1, 2, 3, 4, 5]|slice(1, 2) %}
{{ key }} - {{ value }}
{% endfor %}
{# output
0 - 2
1 - 3
#}
.. note::
It also works with objects implementing the `Traversable`_ interface.
@@ -63,7 +85,7 @@ Arguments
* ``start``: The start of the slice
* ``length``: The size of the slice
* ``preserve_keys``: Whether to preserve key or not (when the input is an array)
* ``preserve_keys``: Whether to preserve key or not (when the input is an array), by default the value is ``false``.
.. _`Traversable`: https://www.php.net/manual/en/class.traversable.php
.. _`array_slice`: https://www.php.net/array_slice
+6
View File
@@ -8,3 +8,9 @@ The ``upper`` filter converts a value to uppercase:
{{ 'welcome'|upper }}
{# outputs 'WELCOME' #}
.. note::
Internally, Twig uses the PHP `mb_strtoupper`_ function.
.. _`mb_strtoupper`: https://www.php.net/mb_strtoupper
+20 -8
View File
@@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SourcePolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;
@@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension
private bool $sandboxedGlobally;
private bool $sandboxed = false;
private SecurityPolicyInterface $policy;
private SourcePolicyInterface $sourcePolicy;
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, SourcePolicyInterface $sourcePolicy = null)
{
$this->policy = $policy;
$this->sandboxedGlobally = $sandboxed;
$this->sourcePolicy = $sourcePolicy;
}
public function getTokenParsers(): array
@@ -50,9 +53,9 @@ final class SandboxExtension extends AbstractExtension
$this->sandboxed = false;
}
public function isSandboxed(): bool
public function isSandboxed(Source $source = null): bool
{
return $this->sandboxedGlobally || $this->sandboxed;
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
}
public function isSandboxedGlobally(): bool
@@ -60,6 +63,15 @@ final class SandboxExtension extends AbstractExtension
return $this->sandboxedGlobally;
}
private function isSourceSandboxed(?Source $source): bool
{
if (null === $source || null === $this->sourcePolicy) {
return false;
}
return $this->sourcePolicy->enableSandbox($source);
}
public function setSecurityPolicy(SecurityPolicyInterface $policy)
{
$this->policy = $policy;
@@ -70,16 +82,16 @@ final class SandboxExtension extends AbstractExtension
return $this->policy;
}
public function checkSecurity($tags, $filters, $functions): void
public function checkSecurity($tags, $filters, $functions, Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
$this->policy->checkSecurity($tags, $filters, $functions);
}
}
public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
@@ -93,7 +105,7 @@ final class SandboxExtension extends AbstractExtension
public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
@@ -107,7 +119,7 @@ final class SandboxExtension extends AbstractExtension
public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
{
if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) {
try {
$this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {
+2 -1
View File
@@ -58,7 +58,8 @@ class CheckSecurityNode extends Node
->indent()
->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n")
->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n")
->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n")
->write(!$functions ? "[],\n" : "['".implode("', '", array_keys($functions))."'],\n")
->write("\$this->source\n")
->outdent()
->write(");\n")
->outdent()
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
use Twig\Source;
/**
* Interface for a class that can optionally enable the sandbox mode based on a template's Twig\Source.
*
* @author Yaakov Saxon
*/
interface SourcePolicyInterface
{
public function enableSandbox(Source $source): bool;
}
+39 -3
View File
@@ -24,6 +24,7 @@ use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityPolicy;
use Twig\Source;
class SandboxTest extends TestCase
{
@@ -440,7 +441,7 @@ EOF
$twig_parent_first->load('1_childobj_childmethod')->render(self::$params);
} catch (SecurityError $e) {
$this->fail('checkMethodAllowed is exiting prematurely after matching a parent class and not seeing a method allowed on a child class later in the list');
}
}
try {
$twig_child_first->load('1_childobj_parentmethod')->render(self::$params);
@@ -449,15 +450,50 @@ EOF
}
}
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [])
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null)
{
$loader = new ArrayLoader($templates);
$twig = new Environment($loader, array_merge(['debug' => true, 'cache' => false, 'autoescape' => false], $options));
$policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$twig->addExtension(new SandboxExtension($policy, $sandboxed));
$twig->addExtension(new SandboxExtension($policy, $sandboxed, $sourcePolicy));
return $twig;
}
public function testSandboxSourcePolicyEnableReturningFalse()
{
$twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return '1_basic' != $source->getName();
}
});
$this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params));
}
public function testSandboxSourcePolicyEnableReturningTrue()
{
$twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return '1_basic' === $source->getName();
}
});
$this->expectException(SecurityError::class);
$twig->load('1_basic')->render([]);
}
public function testSandboxSourcePolicyFalseDoesntOverrideOtherEnables()
{
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return false;
}
});
$this->expectException(SecurityError::class);
$twig->load('1_basic')->render([]);
}
}
class ParentClass