Skip to content

Commit

Permalink
Merge branch '3.x' into 4.x
Browse files Browse the repository at this point in the history
* 3.x:
  Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
  add docs & links
  • Loading branch information
fabpot committed Dec 19, 2023
2 parents e8c511e + 204cd6f commit d27c508
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 24 deletions.
51 changes: 42 additions & 9 deletions doc/filters/batch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand All @@ -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>

Expand All @@ -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``)
5 changes: 3 additions & 2 deletions doc/filters/date.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -68,11 +68,12 @@ 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
.. _`DateTime`: https://www.php.net/DateTime
.. _`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 changes: 6 additions & 0 deletions doc/filters/keys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 6 additions & 0 deletions doc/filters/lower.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 6 additions & 0 deletions doc/filters/reduce.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 23 additions & 1 deletion doc/filters/slice.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions doc/filters/upper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 20 additions & 8 deletions src/Extension/SandboxExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SourcePolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;

Expand All @@ -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
Expand All @@ -50,16 +53,25 @@ public function disableSandbox(): void
$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
{
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;
Expand All @@ -70,16 +82,16 @@ public function getSecurityPolicy(): SecurityPolicyInterface
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) {
Expand All @@ -93,7 +105,7 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour

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) {
Expand All @@ -107,7 +119,7 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $

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) {
Expand Down
3 changes: 2 additions & 1 deletion src/Node/CheckSecurityNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public function compile(Compiler $compiler): void
->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()
Expand Down
24 changes: 24 additions & 0 deletions src/Sandbox/SourcePolicyInterface.php
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 39 additions & 3 deletions tests/Extension/SandboxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityPolicy;
use Twig\Source;

class SandboxTest extends TestCase
{
Expand Down Expand Up @@ -440,7 +441,7 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods()
$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);
Expand All @@ -449,15 +450,50 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods()
}
}

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
Expand Down

0 comments on commit d27c508

Please sign in to comment.