mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-06 23:48:50 +00:00
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:
+42
-9
@@ -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``)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user