TwigSandboxTest.php

Same filename and directory in other branches
  1. 11.x core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php
  2. 10 core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php
  3. 9 core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php
  4. 8.9.x core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php

Namespace

Drupal\Tests\Core\Template

File

core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Tests\Core\Template;

use Drupal\Core\Template\Attribute;
use Drupal\Core\Template\Attribute\TwigAllowed;
use Drupal\Core\Template\Loader\StringLoader;
use Drupal\Core\Template\TwigSandboxPolicy;
use Drupal\Tests\Core\Entity\ContentEntityBaseMockableClass;
use Drupal\Tests\UnitTestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
use Twig\Sandbox\SecurityError;

/**
 * Tests the twig sandbox policy.
 */
class TwigSandboxTest extends UnitTestCase {
  
  /**
   * The Twig environment loaded with the sandbox extension.
   *
   * @var \Twig\Environment
   */
  protected $twig;
  
  /**
   * {@inheritdoc}
   */
  protected function setUp() : void {
    parent::setUp();
    $loader = new StringLoader();
    $this->twig = new Environment($loader);
    $policy = new TwigSandboxPolicy();
    $sandbox = new SandboxExtension($policy, TRUE);
    $this->twig
      ->addExtension($sandbox);
  }
  
  /**
   * Tests that dangerous methods cannot be called in entity objects.
   */
  public function testEntityDangerousMethods($template) : void {
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $this->expectException(SecurityError::class);
    $this->twig
      ->render($template, [
      'entity' => $entity,
    ]);
  }
  
  /**
   * Data provider for ::testEntityDangerousMethods.
   *
   * @return array
   *   An array of dangerous methods.
   */
  public static function getTwigEntityDangerousMethods() : array {
    return [
      [
        '{{ entity.delete }}',
      ],
      [
        '{{ entity.save }}',
      ],
      [
        '{{ entity.create }}',
      ],
    ];
  }
  
  /**
   * Tests that white listed classes can be extended.
   */
  public function testExtendedClass() : void {
    $this->assertEquals(' class=&quot;kitten&quot;', $this->twig
      ->render('{{ attribute.addClass("kitten") }}', [
      'attribute' => new TestAttribute(),
    ]));
  }
  
  /**
   * Tests that prefixed methods can be called from within Twig templates.
   *
   * Currently "get", "has", and "is" are the only allowed prefixes.
   */
  public function testEntitySafePrefixes() : void {
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $entity->expects($this->atLeastOnce())
      ->method('hasLinkTemplate')
      ->with('test')
      ->willReturn(TRUE);
    $result = $this->twig
      ->render('{{ entity.hasLinkTemplate("test") }}', [
      'entity' => $entity,
    ]);
    $this->assertTrue((bool) $result, 'Sandbox policy allows has* functions to be called.');
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $entity->expects($this->atLeastOnce())
      ->method('isNew')
      ->willReturn(TRUE);
    $result = $this->twig
      ->render('{{ entity.isNew }}', [
      'entity' => $entity,
    ]);
    $this->assertTrue((bool) $result, 'Sandbox policy allows is* functions to be called.');
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $entity->expects($this->atLeastOnce())
      ->method('getEntityType')
      ->willReturn('test');
    $result = $this->twig
      ->render('{{ entity.getEntityType }}', [
      'entity' => $entity,
    ]);
    $this->assertEquals('test', $result, 'Sandbox policy allows get* functions to be called.');
  }
  
  /**
   * Tests that valid methods can be called from within Twig templates.
   *
   * Currently the following methods are in the allowed list: id, label, bundle,
   * and get.
   */
  public function testEntitySafeMethods() : void {
    $entity = $this->getMockBuilder(ContentEntityBaseMockableClass::class)
      ->disableOriginalConstructor()
      ->getMock();
    $entity->expects($this->atLeastOnce())
      ->method('get')
      ->with('title')
      ->willReturn('test');
    $result = $this->twig
      ->render('{{ entity.get("title") }}', [
      'entity' => $entity,
    ]);
    $this->assertEquals('test', $result, 'Sandbox policy allows get() to be called.');
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $entity->expects($this->atLeastOnce())
      ->method('id')
      ->willReturn('1234');
    $result = $this->twig
      ->render('{{ entity.id }}', [
      'entity' => $entity,
    ]);
    $this->assertEquals('1234', $result, 'Sandbox policy allows get() to be called.');
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $entity->expects($this->atLeastOnce())
      ->method('label')
      ->willReturn('testing');
    $result = $this->twig
      ->render('{{ entity.label }}', [
      'entity' => $entity,
    ]);
    $this->assertEquals('testing', $result, 'Sandbox policy allows get() to be called.');
    $entity = $this->createMock('Drupal\\Core\\Entity\\EntityInterface');
    $entity->expects($this->atLeastOnce())
      ->method('bundle')
      ->willReturn('testing');
    $result = $this->twig
      ->render('{{ entity.bundle }}', [
      'entity' => $entity,
    ]);
    $this->assertEquals('testing', $result, 'Sandbox policy allows get() to be called.');
  }
  
  /**
   * Tests that safe methods inside Url objects can be called.
   */
  public function testUrlSafeMethods() : void {
    $url = $this->getMockBuilder('Drupal\\Core\\Url')
      ->disableOriginalConstructor()
      ->getMock();
    $url->expects($this->once())
      ->method('toString')
      ->willReturn('http://kittens.cat/are/cute');
    $result = $this->twig
      ->render('{{ url.toString }}', [
      'url' => $url,
    ]);
    $this->assertEquals('http://kittens.cat/are/cute', $result, 'Sandbox policy allows toString() to be called.');
  }
  
  /**
   * Tests that method with TwigAllowed attribute is allowed.
   */
  public function testTwigMethodAttributeAllowed() : void {
    $object = new AttributeAllowTestClass();
    $result = $this->twig
      ->render('{{ object.allowed() }}', [
      'object' => $object,
    ]);
    $this->assertTrue((bool) $result, 'TwigAllowed attribute allows method to be called.');
  }
  
  /**
   * Tests that method without TwigAllowed attribute is not allowed.
   */
  public function testTwigMethodAttributeNotAllowed() : void {
    $object = new AttributeAllowTestClass();
    $this->expectException(SecurityError::class);
    $this->twig
      ->render('{{ object.notAllowed() }}', [
      'object' => $object,
    ]);
  }

}

/**
 * Test class for HTML attributes collector, sanitizer, and renderer.
 */
class TestAttribute extends Attribute {

}

/**
 * Test class for TwigAllowed attribute.
 */
class AttributeAllowTestClass {
  public function allowed() : string {
    return __METHOD__;
  }
  public function notAllowed() : string {
    return __METHOD__;
  }

}

Classes

Title Deprecated Summary
AttributeAllowTestClass Test class for TwigAllowed attribute.
TestAttribute Test class for HTML attributes collector, sanitizer, and renderer.
TwigSandboxTest Tests the twig sandbox policy.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.