Files
core/src/Services/TwigSceneRenderer.php
T

135 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace LotGD\Core\Services;
use LotGD\Core\Events\EventContextData;
use LotGD\Core\Exceptions\CharacterNotFoundException;
use LotGD\Core\Exceptions\InsecureTwigTemplateError;
use LotGD\Core\Game;
use LotGD\Core\Models\Character;
use LotGD\Core\Models\Scene;
use LotGD\Core\Models\Viewpoint;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\Extension\SandboxExtension;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityPolicy;
class TwigSceneRenderer
{
private Environment $twig;
private array $templateValues;
public function __construct(
private Game $game
) {
$this->twig = new Environment(new TwigNullLoader());
// Add a hook for additional fields
// This is for global changes only. Viewpoint-dependent changes should try to store the important values within
// the viewpoint itself.
$eventManager = $this->game->getEventManager();
$contextData = EventContextData::create(["templateValues" => []]);
$newContextData = $eventManager->publish("h/lotgd/core/scene-renderer/templateValues", $contextData);
$this->templateValues = $newContextData->get("templateValues") ?? [];
// Add Sandbox extension
$securityPolicy = $this->getSecurityPolicy();
$this->twig->addExtension(new SandboxExtension($securityPolicy, sandboxed: true));
}
/**
* Renders a given string in the context if a given viewpoint.
*
* @param string $string
* @param Viewpoint $viewpoint
* @param bool $ignoreErrors If set to true, errors are ignored and the unparsed string will be returned instead.
* @return string
* @throws InsecureTwigTemplateError
* @throws CharacterNotFoundException
* @throws LoaderError
* @throws SyntaxError
*/
public function render(string $string, Viewpoint $viewpoint, bool $ignoreErrors = false): string
{
// We catch here "Tag" errors. If error, we'll exit either by returning the input ($ignoreError === true) or
// throwing an exception.
try {
$template = $this->twig->createTemplate($string);
} catch (SecurityError $e) {
if ($ignoreErrors) {
return $string;
} else {
throw new InsecureTwigTemplateError("Template contains illegal calls: {$e->getMessage()}");
}
}
$templateValues = [
"Character" => $this->game->getCharacter(),
"Scene" => $viewpoint->getScene(),
"Viewpoint" => $viewpoint,
];
// Merges additional template values with important ones.
$templateValues = array_merge($this->templateValues, $templateValues);
// Try to render the template
try {
// This could 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;
}
/**
* Returns the current security policy.
* This method provides a hook.
* @return SecurityPolicy
*/
public function getSecurityPolicy(): SecurityPolicy
{
$tags = ["if"];
$filters = ["lower", "upper", "escape"];
$functions = ["range"];
$methods = [
Character::class => ["getDisplayName", "getLevel", "isAlive", "getHealth", "getMaxHealth", "getProperty"],
Scene::class => ["getProperty"],
Viewpoint::class => ["getData"],
];
$properties = [
"Character" => ["displayName", "level", "alive", "health", "maxHealth", "property"],
"Viewpoint" => ["data"],
];
// Publish event to change $templateValues
$eventManager = $this->game->getEventManager();
$contextData = EventContextData::create([
"tags" => $tags,
"filters" => $filters,
"functions" => $functions,
"methods" => $methods,
"properties" => $properties,
]);
$newContextData = $eventManager->publish("h/lotgd/core/scene-renderer/securityPolicy", $contextData);
// Set changed values from the event.
$tags = $newContextData->get("tags");
$filters = $newContextData->get("filters");
$functions = $newContextData->get("functions");
$methods = $newContextData->get("methods");
$properties = $newContextData->get("properties");
return new SecurityPolicy($tags, $filters, $methods, $properties, $functions);
}
}