diff --git a/composer.json b/composer.json index bfe861f..f8480ec 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "monolog/monolog": "^2.0", "symfony/console": "^5.0", "symfony/yaml": "^5.0", + "twig/twig": "^3.0", "ramsey/uuid-doctrine": "^1.5", "jetbrains/phpstorm-attributes": "^1.0" }, diff --git a/composer.lock b/composer.lock index 7d100f2..4c1fc11 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9cab3ccd48d3179741e9597ca856fb08", + "content-hash": "948d1f1c6837be4e8ff616c5e7ee5924", "packages": [ { "name": "brick/math", @@ -64,16 +64,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.8", + "version": "1.2.9", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "8a7ecad675253e4654ea05505233285377405215" + "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", - "reference": "8a7ecad675253e4654ea05505233285377405215", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5", "shasum": "" }, "require": { @@ -82,14 +82,15 @@ "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", + "phpstan/phpstan": "^0.12.55", "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -119,7 +120,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.8" + "source": "https://github.com/composer/ca-bundle/tree/1.2.9" }, "funding": [ { @@ -135,7 +136,7 @@ "type": "tidelift" } ], - "time": "2020-08-23T12:54:47+00:00" + "time": "2021-01-12T12:10:35+00:00" }, { "name": "composer/composer", @@ -2524,16 +2525,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { @@ -2545,7 +2546,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2583,7 +2584,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.0" }, "funding": [ { @@ -2599,20 +2600,20 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c" + "reference": "267a9adeb8ecb8071040a740930e077cdfb987af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", - "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/267a9adeb8ecb8071040a740930e077cdfb987af", + "reference": "267a9adeb8ecb8071040a740930e077cdfb987af", "shasum": "" }, "require": { @@ -2624,7 +2625,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2664,7 +2665,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.0" }, "funding": [ { @@ -2680,20 +2681,20 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "727d1096295d807c309fb01a851577302394c897" + "reference": "6e971c891537eb617a00bb07a43d182a6915faba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", - "reference": "727d1096295d807c309fb01a851577302394c897", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", + "reference": "6e971c891537eb617a00bb07a43d182a6915faba", "shasum": "" }, "require": { @@ -2705,7 +2706,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2748,7 +2749,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.0" }, "funding": [ { @@ -2764,20 +2765,20 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T17:09:11+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", "shasum": "" }, "require": { @@ -2789,7 +2790,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2828,7 +2829,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.0" }, "funding": [ { @@ -2844,20 +2845,20 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", "shasum": "" }, "require": { @@ -2866,7 +2867,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2907,7 +2908,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.0" }, "funding": [ { @@ -2923,20 +2924,20 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", "shasum": "" }, "require": { @@ -2945,7 +2946,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2990,7 +2991,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.0" }, "funding": [ { @@ -3006,7 +3007,7 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/process", @@ -3306,6 +3307,82 @@ } ], "time": "2020-12-08T17:02:38+00:00" + }, + { + "name": "twig/twig", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "f795ca686d38530045859b0350b5352f7d63447d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/f795ca686d38530045859b0350b5352f7d63447d", + "reference": "f795ca686d38530045859b0350b5352f7d63447d", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2021-01-05T15:40:36+00:00" } ], "packages-dev": [ @@ -5656,16 +5733,16 @@ }, { "name": "symfony/polyfill-php72", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", "shasum": "" }, "require": { @@ -5674,7 +5751,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5712,7 +5789,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.0" }, "funding": [ { @@ -5728,7 +5805,7 @@ "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/stopwatch", diff --git a/src/Exceptions/InsecureTwigTemplateError.php b/src/Exceptions/InsecureTwigTemplateError.php new file mode 100644 index 0000000..01452ae --- /dev/null +++ b/src/Exceptions/InsecureTwigTemplateError.php @@ -0,0 +1,8 @@ +twig = new Environment(new TwigNullLoader()); + + $securityPolicy = $this->getSecurityPolicy(); + + # Add Sandbox extension + $this->twig->addExtension(new SandboxExtension($securityPolicy, sandboxed: true)); + } + + public function render(string $string, Scene $scene, bool $ignoreErrors = false): string + { + $template = $this->twig->createTemplate($string); + + $templateValues = [ + "Character" => $this->game->getCharacter(), + "Scene" => $scene, + ]; + + // @Todo: Event to add property to the template + + try { + // This will throw a SecurityError + $result = $template->render($templateValues); + } catch (SecurityError $e) { + if ($ignoreErrors) { + return $string; + } else { + throw new InsecureTwigTemplateError("Template contains illegal calls: {$e->getMessage()}"); + } + } + + return $result; + } + + public function getSecurityPolicy(): SecurityPolicy + { + $tags = ["if"]; + $filters = ["lower", "upper", "escape"]; + $functions = ["range"]; + $methods = [ + Character::class => ["getDisplayName", "getLevel", "isAlive", "getHealth", "getMaxHealth", "getProperty"], + Scene::class => ["getProperty"], + ]; + $properties = [ + "Character" => ["displayName", "level", "health", "maxHealth"], + ]; + + // @ToDo: Event to change Security Policy + + return new SecurityPolicy($tags, $filters, $methods, $properties, $functions); + } +} \ No newline at end of file diff --git a/tests/Managers/PermissionManagerTest.php b/tests/Services/PermissionManagerTest.php similarity index 99% rename from tests/Managers/PermissionManagerTest.php rename to tests/Services/PermissionManagerTest.php index 721aa50..f03abd9 100644 --- a/tests/Managers/PermissionManagerTest.php +++ b/tests/Services/PermissionManagerTest.php @@ -1,7 +1,7 @@ getMockBuilder(Game::class) + ->disableOriginalConstructor() + ->getMock(); + + return $game; + } + + public function testIfSceneRendererCanBeConstructed() + { + $renderer = new TwigSceneRenderer($this->getGameMock()); + + $this->assertInstanceOf(TwigSceneRenderer::class, $renderer); + } + + public function testIfTwigSceneRendererReturnsANonTemplateStringUnmodified() + { + # Get renderer + $renderer = new TwigSceneRenderer($this->getGameMock()); + + # Get mock scene + $scene = $this->getMockBuilder(Scene::class) + ->disableOriginalConstructor() + ->getMock(); + + # Prepare the template string. + $template = "You enter a new location.\n\nA new location."; + + # Create the result + $renderResult = $renderer->render($template, $scene); + + # Assert result + $this->assertSame($template, $renderResult); + } + + public function testIfTwigSceneRendererParsesStringsWithCharacters() + { + # Get mock character + $character = $this->getMockBuilder(Character::class) + ->disableOriginalConstructor() + ->getMock(); + $character->method("getDisplayName")->willReturn("Frodo"); + $character->method("getLevel")->willReturn(5); + $character->method("getHealth")->willReturn(10); + $character->method("getMaxHealth")->willReturn(100); + $character->method("isAlive")->willReturn(true); + + # Get mock game + $game = $this->getMockBuilder(Game::class) + ->disableOriginalConstructor() + ->getMock(); + $game->method("getCharacter")->willReturn($character); + + # Get mock scene + $scene = $this->getMockBuilder(Scene::class) + ->disableOriginalConstructor() + ->getMock(); + + # Get renderer + $renderer = new TwigSceneRenderer($game); + + # Prepare the template string. + $template = "Hi {{ Character.getDisplayName }}! How are you today? Your level is {{ Character.level}}, and you have " + ."{{ Character.health }} out of {{ Character.maxHealth }} health points." + ."{% if Character.isAlive %} You are alive.{% endif %}"; + + $result = "Hi Frodo! How are you today? Your level is 5, and you have " + ."10 out of 100 health points. " + ."You are alive."; + + # Create the result + $renderResult = $renderer->render($template, $scene); + + # Assert result + $this->assertSame($result, $renderResult); + } + + public function testIfRawTemplateGetsReturnedIfTemplateContainsIllegalTokens() + { + # Get mock character + $character = $this->getMockBuilder(Character::class) + ->disableOriginalConstructor() + ->getMock(); + $character->method("getDisplayName")->willReturn("Frodo"); + $character->method("getLevel")->willReturn(5); + $character->method("getHealth")->willReturn(10); + $character->method("getMaxHealth")->willReturn(100); + $character->method("isAlive")->willReturn(true); + + # Get mock game + $game = $this->getMockBuilder(Game::class) + ->disableOriginalConstructor() + ->getMock(); + $game->method("getCharacter")->willReturn($character); + + # Get mock scene + $scene = $this->getMockBuilder(Scene::class) + ->disableOriginalConstructor() + ->getMock(); + + # Get renderer + $renderer = new TwigSceneRenderer($game); + + # Prepare the template string. + $template = "Viewpoint: {{ Character.viewpoint }}"; + + # Try to parse the result + $renderResult = $renderer->render($template, $scene, true); + + # If there was an error, it should have gotten ignored, giving back the raw template. + $this->assertSame($template, $renderResult); + } + + public function testIfExceptionGetsRaisedIfTemplateContainsIllegalTokens() + { + # Get mock character + $character = $this->getMockBuilder(Character::class) + ->disableOriginalConstructor() + ->getMock(); + $character->method("getDisplayName")->willReturn("Frodo"); + $character->method("getLevel")->willReturn(5); + $character->method("getHealth")->willReturn(10); + $character->method("getMaxHealth")->willReturn(100); + $character->method("isAlive")->willReturn(true); + + # Get mock game + $game = $this->getMockBuilder(Game::class) + ->disableOriginalConstructor() + ->getMock(); + $game->method("getCharacter")->willReturn($character); + + # Get mock scene + $scene = $this->getMockBuilder(Scene::class) + ->disableOriginalConstructor() + ->getMock(); + + # Get renderer + $renderer = new TwigSceneRenderer($game); + + # Prepare the template string. + $template = "Viewpoint: {{ Character.viewpoint }}"; + + # Prepare the exception expectation + $this->expectException(InsecureTwigTemplateError::class); + + # Try to parse the result + $renderResult = $renderer->render($template, $scene, false); + } +} \ No newline at end of file