diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index 59950e258c..d2fa63a562 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -5,6 +5,7 @@ use Lang; use File; use Event; +use Config; use Backend; use BackendMenu; use BackendAuth; @@ -17,7 +18,12 @@ use System\Classes\CombineAssets; use Cms\Classes\Theme as CmsTheme; use Backend\Classes\WidgetManager; +use System\Classes\MarkupManager; use System\Classes\SettingsManager; +use Twig\Cache\FilesystemCache as TwigCacheFilesystem; +use Cms\Twig\Loader as CmsTwigLoader; +use Cms\Twig\DebugExtension; +use Cms\Twig\Extension as CmsTwigExtension; use Winter\Storm\Support\ModuleServiceProvider; @@ -31,6 +37,7 @@ class ServiceProvider extends ModuleServiceProvider public function register() { $this->registerConsole(); + $this->registerTwigParser(); $this->registerAssetBundles(); $this->registerComponents(); $this->registerThemeLogging(); @@ -78,6 +85,43 @@ protected function registerConsole() $this->registerConsoleCommand('theme.sync', \Cms\Console\ThemeSync::class); } + /* + * Register Twig Environments and other Twig modifications provided by the module + */ + protected function registerTwigParser() + { + // Register CMS Twig environment + App::singleton('twig.environment.cms', function ($app) { + // Load Twig options + $useCache = !Config::get('cms.twigNoCache'); + $isDebugMode = Config::get('app.debug', false); + $strictVariables = Config::get('cms.enableTwigStrictVariables', false); + $strictVariables = $strictVariables ?? $isDebugMode; + $forceBytecode = Config::get('cms.forceBytecodeInvalidation', false); + + $options = [ + 'auto_reload' => true, + 'debug' => $isDebugMode, + 'strict_variables' => $strictVariables, + ]; + + if ($useCache) { + $options['cache'] = new TwigCacheFilesystem( + storage_path().'/cms/twig', + $forceBytecode ? TwigCacheFilesystem::FORCE_BYTECODE_INVALIDATION : 0 + ); + } + + $twig = MarkupManager::makeBaseTwigEnvironment(new CmsTwigLoader, $options); + $twig->addExtension(new CmsTwigExtension); + if ($isDebugMode) { + $twig->addExtension(new DebugExtension); + } + + return $twig; + }); + } + /** * Register asset bundles */ diff --git a/modules/cms/classes/CmsCompoundObject.php b/modules/cms/classes/CmsCompoundObject.php index ce7069ae30..acfc6fa684 100644 --- a/modules/cms/classes/CmsCompoundObject.php +++ b/modules/cms/classes/CmsCompoundObject.php @@ -4,14 +4,10 @@ use Lang; use Cache; use Config; -use Cms\Twig\Loader as TwigLoader; -use Cms\Twig\Extension as CmsTwigExtension; use Cms\Components\ViewBag; use Cms\Helpers\Cms as CmsHelpers; -use System\Twig\Extension as SystemTwigExtension; use Winter\Storm\Halcyon\Processors\SectionParser; use Twig\Source as TwigSource; -use Twig\Environment as TwigEnvironment; use ApplicationException; /** @@ -107,7 +103,7 @@ public function beforeSave() $this->code = $this->getOriginal('code'); } } - + $this->checkSafeMode(); } @@ -414,11 +410,7 @@ public function getTwigContent() */ public function getTwigNodeTree($markup = false) { - $loader = new TwigLoader(); - $twig = new TwigEnvironment($loader, []); - $twig->addExtension(new CmsTwigExtension()); - $twig->addExtension(new SystemTwigExtension); - + $twig = App::make('twig.environment.cms'); $stream = $twig->tokenize(new TwigSource($markup === false ? $this->markup : $markup, 'getTwigNodeTree')); return $twig->parse($stream); } diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index f982b65024..b4b0bef877 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -14,17 +14,10 @@ use SystemException; use BackendAuth; use Twig\Environment as TwigEnvironment; -use Twig\Cache\FilesystemCache as TwigCacheFilesystem; -use Twig\Extension\SandboxExtension; -use Cms\Twig\Loader as TwigLoader; -use Cms\Twig\DebugExtension; -use Cms\Twig\Extension as CmsTwigExtension; use Cms\Models\MaintenanceSetting; use System\Models\RequestLog; use System\Helpers\View as ViewHelper; use System\Classes\CombineAssets; -use System\Twig\Extension as SystemTwigExtension; -use System\Twig\SecurityPolicy; use Winter\Storm\Exception\AjaxException; use Winter\Storm\Exception\ValidationException; use Winter\Storm\Parse\Bracket as TextParser; @@ -54,11 +47,6 @@ class Controller */ protected $router; - /** - * @var \Cms\Twig\Loader A reference to the Twig template loader. - */ - protected $loader; - /** * @var \Cms\Classes\Page A reference to the CMS page template being processed. */ @@ -429,8 +417,8 @@ public function runPage($page, $useAjax = true) * Render the page */ CmsException::mask($this->page, 400); - $this->loader->setObject($this->page); - $template = $this->twig->loadTemplate($this->page->getFilePath()); + $this->getLoader()->setObject($this->page); + $template = $this->getTwig()->load($this->page->getFilePath()); $this->pageContents = $template->render($this->vars); CmsException::unmask(); } @@ -439,8 +427,8 @@ public function runPage($page, $useAjax = true) * Render the layout */ CmsException::mask($this->layout, 400); - $this->loader->setObject($this->layout); - $template = $this->twig->loadTemplate($this->layout->getFilePath()); + $this->getLoader()->setObject($this->layout); + $template = $this->getTwig()->load($this->layout->getFilePath()); $result = $template->render($this->vars); CmsException::unmask(); @@ -595,35 +583,7 @@ protected function postProcessResult($page, $url, $content) */ protected function initTwigEnvironment() { - $this->loader = new TwigLoader; - - $useCache = !Config::get('cms.twigNoCache'); - $isDebugMode = Config::get('app.debug', false); - $strictVariables = Config::get('cms.enableTwigStrictVariables', false); - $strictVariables = $strictVariables ?? $isDebugMode; - $forceBytecode = Config::get('cms.forceBytecodeInvalidation', false); - - $options = [ - 'auto_reload' => true, - 'debug' => $isDebugMode, - 'strict_variables' => $strictVariables, - ]; - - if ($useCache) { - $options['cache'] = new TwigCacheFilesystem( - storage_path().'/cms/twig', - $forceBytecode ? TwigCacheFilesystem::FORCE_BYTECODE_INVALIDATION : 0 - ); - } - - $this->twig = new TwigEnvironment($this->loader, $options); - $this->twig->addExtension(new CmsTwigExtension($this)); - $this->twig->addExtension(new SystemTwigExtension); - $this->twig->addExtension(new SandboxExtension(new SecurityPolicy, true)); - - if ($isDebugMode) { - $this->twig->addExtension(new DebugExtension($this)); - } + $this->twig = App::make('twig.environment.cms'); } /** @@ -1096,8 +1056,8 @@ public function renderPartial($name, $parameters = [], $throwException = true) * Render the partial */ CmsException::mask($partial, 400); - $this->loader->setObject($partial); - $template = $this->twig->loadTemplate($partial->getFilePath()); + $this->getLoader()->setObject($partial); + $template = $this->getTwig()->load($partial->getFilePath()); $partialContent = $template->render(array_merge($this->vars, $parameters)); CmsException::unmask(); @@ -1274,7 +1234,7 @@ public function getTwig() */ public function getLoader() { - return $this->loader; + return $this->getTwig()->getLoader(); } /** diff --git a/modules/cms/twig/ComponentNode.php b/modules/cms/twig/ComponentNode.php index 4787fe9b2d..e91e2bbca4 100644 --- a/modules/cms/twig/ComponentNode.php +++ b/modules/cms/twig/ComponentNode.php @@ -34,7 +34,7 @@ public function compile(TwigCompiler $compiler) } $compiler - ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->componentFunction(") + ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->componentFunction(\$context,") ->subcompile($this->getNode('nodes')->getNode(0)) ->write(", \$context['__cms_component_params']") ->write(");\n") diff --git a/modules/cms/twig/ContentNode.php b/modules/cms/twig/ContentNode.php index f1f9df8602..a6880951eb 100644 --- a/modules/cms/twig/ContentNode.php +++ b/modules/cms/twig/ContentNode.php @@ -36,7 +36,7 @@ public function compile(TwigCompiler $compiler) } $compiler - ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->contentFunction(") + ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->contentFunction(\$context,") ->subcompile($this->getNode('nodes')->getNode(0)) ->write(", \$context['__cms_content_params']") ->write(");\n") diff --git a/modules/cms/twig/DebugExtension.php b/modules/cms/twig/DebugExtension.php index 67b03730f9..11aed1bd0d 100644 --- a/modules/cms/twig/DebugExtension.php +++ b/modules/cms/twig/DebugExtension.php @@ -4,7 +4,6 @@ use Twig\Extension\AbstractExtension as TwigExtension; use Twig\Environment as TwigEnvironment; use Twig\TwigFunction as TwigSimpleFunction; -use Cms\Classes\Controller; use Cms\Classes\ComponentBase; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; @@ -19,11 +18,6 @@ class DebugExtension extends TwigExtension const OBJECT_CAPTION = 'Object variables'; const COMPONENT_CAPTION = 'Component variables'; - /** - * @var \Cms\Classes\Controller A reference to the CMS controller. - */ - protected $controller; - /** * @var integer Helper for rendering table row styles. */ @@ -52,15 +46,6 @@ class DebugExtension extends TwigExtension 'offsetUnset' ]; - /** - * Creates the extension instance. - * @param \Cms\Classes\Controller $controller The CMS controller object. - */ - public function __construct(Controller $controller) - { - $this->controller = $controller; - } - /** * Returns a list of global functions to add to the existing list. * @return array An array of global functions diff --git a/modules/cms/twig/Extension.php b/modules/cms/twig/Extension.php index 867087888c..4d527d4f74 100644 --- a/modules/cms/twig/Extension.php +++ b/modules/cms/twig/Extension.php @@ -5,7 +5,6 @@ use Twig\Extension\AbstractExtension as TwigExtension; use Twig\TwigFilter as TwigSimpleFilter; use Twig\TwigFunction as TwigSimpleFunction; -use Cms\Classes\Controller; /** * The CMS Twig extension class implements the basic CMS Twig functions and filters. @@ -16,54 +15,44 @@ class Extension extends TwigExtension { /** - * @var \Cms\Classes\Controller A reference to the CMS controller. + * Returns an array of functions to add to the existing list. */ - protected $controller; - - /** - * Creates the extension instance. - * @param \Cms\Classes\Controller $controller The CMS controller object. - */ - public function __construct(Controller $controller = null) + public function getFunctions(): array { - $this->controller = $controller; - } + $options = [ + 'is_safe' => ['html'], + 'needs_context' => true, + ]; - /** - * Returns a list of functions to add to the existing list. - * - * @return array An array of functions - */ - public function getFunctions() - { return [ - new TwigSimpleFunction('page', [$this, 'pageFunction'], ['is_safe' => ['html']]), - new TwigSimpleFunction('partial', [$this, 'partialFunction'], ['is_safe' => ['html']]), - new TwigSimpleFunction('content', [$this, 'contentFunction'], ['is_safe' => ['html']]), - new TwigSimpleFunction('component', [$this, 'componentFunction'], ['is_safe' => ['html']]), + new TwigSimpleFunction('page', [$this, 'pageFunction'], $options), + new TwigSimpleFunction('partial', [$this, 'partialFunction'], $options), + new TwigSimpleFunction('content', [$this, 'contentFunction'], $options), + new TwigSimpleFunction('component', [$this, 'componentFunction'], $options), new TwigSimpleFunction('placeholder', [$this, 'placeholderFunction'], ['is_safe' => ['html']]), ]; } /** - * Returns a list of filters this extensions provides. - * - * @return array An array of filters + * Returns an array of filters this extension provides. */ - public function getFilters() + public function getFilters(): array { + $options = [ + 'is_safe' => ['html'], + 'needs_context' => true, + ]; + return [ - new TwigSimpleFilter('page', [$this, 'pageFilter'], ['is_safe' => ['html']]), - new TwigSimpleFilter('theme', [$this, 'themeFilter'], ['is_safe' => ['html']]), + new TwigSimpleFilter('page', [$this, 'pageFilter'], $options), + new TwigSimpleFilter('theme', [$this, 'themeFilter'], $options), ]; } /** - * Returns a list of token parsers this extensions provides. - * - * @return array An array of token parsers + * Returns an array of token parsers this extension provides. */ - public function getTokenParsers() + public function getTokenParsers(): array { return [ new PageTokenParser, @@ -82,64 +71,49 @@ public function getTokenParsers() } /** - * Renders a page. - * This function should be used in the layout code to output the requested page. - * @return string Returns the page contents. + * Renders a page; used in the layout code to output the requested page. */ - public function pageFunction() + public function pageFunction(array $context): string { - return $this->controller->renderPage(); + return $context['this']['controller']->renderPage(); } /** - * Renders a partial. - * @param string $name Specifies the partial name. - * @param array $parameters A optional list of parameters to pass to the partial. - * @param bool $throwException Throw an exception if the partial is not found. - * @return string Returns the partial contents. + * Renders the requested partial with the provided parameters. Optionally throw an exception if the partial cannot be found */ - public function partialFunction($name, $parameters = [], $throwException = false) + public function partialFunction(array $context, string $name, array $parameters = [], bool $throwException = false): string { - return $this->controller->renderPartial($name, $parameters, $throwException); + return $context['this']['controller']->renderPartial($name, $parameters, $throwException); } /** - * Renders a content file. - * @param string $name Specifies the content block name. - * @param array $parameters A optional list of parameters to pass to the content. - * @return string Returns the file contents. + * Renders the requested content file. */ - public function contentFunction($name, $parameters = []) + public function contentFunction(array $context, string $name, array $parameters = []): string { - return $this->controller->renderContent($name, $parameters); + return $context['this']['controller']->renderContent($name, $parameters); } /** - * Renders a component's default content. - * @param string $name Specifies the component name. - * @param array $parameters A optional list of parameters to pass to the component. - * @return string Returns the component default contents. + * Renders a component's default partial. */ - public function componentFunction($name, $parameters = []) + public function componentFunction(array $context, string $name, array $parameters = []): string { - return $this->controller->renderComponent($name, $parameters); + return $context['this']['controller']->renderComponent($name, $parameters); } /** - * Renders registered assets of a given type - * @return string Returns the component default contents. + * Renders registered assets of a given type or all types if $type not provided */ - public function assetsFunction($type = null) + public function assetsFunction(array $context, string $type = null): ?string { - return $this->controller->makeAssets($type); + return $context['this']['controller']->makeAssets($type); } /** - * Renders a placeholder content, without removing the block, - * must be called before the placeholder tag itself - * @return string Returns the placeholder contents. + * Renders placeholder content, without removing the block, must be called before the placeholder tag itself */ - public function placeholderFunction($name, $default = null) + public function placeholderFunction(string $name, string $default = null): string { if (($result = Block::get($name)) === null) { return null; @@ -150,45 +124,42 @@ public function placeholderFunction($name, $default = null) } /** - * Looks up the URL for a supplied page and returns it relative to the website root. + * Returns the relative URL for the provided page + * + * @param array $context The Twig context for the call (relies on $context['this']['controller'] to exist) * @param mixed $name Specifies the Cms Page file name. * @param array $parameters Route parameters to consider in the URL. - * @param bool $routePersistence By default the existing routing parameters will be included - * when creating the URL, set to false to disable this feature. - * @return string + * @param bool $routePersistence Set to false to exclude the existing routing parameters from the generated URL */ - public function pageFilter($name, $parameters = [], $routePersistence = true) + public function pageFilter(array $context, $name, array $parameters = [], $routePersistence = true): string { - return $this->controller->pageUrl($name, $parameters, $routePersistence); + return $context['this']['controller']->pageUrl($name, $parameters, $routePersistence); } /** * Converts supplied URL to a theme URL relative to the website root. If the URL provided is an * array then the files will be combined. - * @param mixed $url Specifies the theme-relative URL - * @return string + * + * @param array $context The Twig context for the call (relies on $context['this']['controller'] to exist) + * @param mixed $url Specifies the input to be turned into a URL (arrays will be passed to the AssetCombiner) */ - public function themeFilter($url) + public function themeFilter(array $context, $url): string { - return $this->controller->themeUrl($url); + return $context['this']['controller']->themeUrl($url); } /** * Opens a layout block. - * @param string $name Specifies the block name */ - public function startBlock($name) + public function startBlock(string $name): void { Block::startBlock($name); } /** - * Returns a layout block contents and removes the block. - * @param string $name Specifies the block name - * @param string $default The default placeholder contents. - * @return mixed Returns the block contents string or null of the block doesn't exist + * Returns a layout block contents (or null if it doesn't exist) and removes the block. */ - public function displayBlock($name, $default = null) + public function displayBlock(string $name, string $default = null): ?string { if (($result = Block::placeholder($name)) === null) { return $default; @@ -218,7 +189,7 @@ public function displayBlock($name, $default = null) /** * Closes a layout block. */ - public function endBlock($append = true) + public function endBlock($append = true): void { Block::endBlock($append); } diff --git a/modules/cms/twig/Loader.php b/modules/cms/twig/Loader.php index 87f13b9fc6..ca20215d26 100644 --- a/modules/cms/twig/Loader.php +++ b/modules/cms/twig/Loader.php @@ -39,11 +39,8 @@ public function setObject(CmsObject $obj) /** * Returns the Twig content string. * This step is cached internally by Twig. - * - * @param string $name The template name - * @return TwigSource */ - public function getSourceContext($name) + public function getSourceContext(string $name): TwigSource { if (!$this->validateCmsObject($name)) { return parent::getSourceContext($name); @@ -70,11 +67,8 @@ public function getSourceContext($name) /** * Returns the Twig cache key. - * - * @param string $name The template name - * @return string */ - public function getCacheKey($name) + public function getCacheKey(string $name): string { if (!$this->validateCmsObject($name)) { return parent::getCacheKey($name); @@ -90,7 +84,7 @@ public function getCacheKey($name) * @param mixed $time The time to check against the template * @return bool */ - public function isFresh($name, $time) + public function isFresh(string $name, int $time): bool { if (!$this->validateCmsObject($name)) { return parent::isFresh($name, $time); @@ -101,11 +95,8 @@ public function isFresh($name, $time) /** * Returns the file name of the loaded template. - * - * @param string $name The template name - * @return string */ - public function getFilename($name) + public function getFilename(string $name): string { if (!$this->validateCmsObject($name)) { return parent::getFilename($name); @@ -116,11 +107,8 @@ public function getFilename($name) /** * Checks that the template exists. - * - * @param string $name The template name - * @return bool */ - public function exists($name) + public function exists(string $name): bool { if (!$this->validateCmsObject($name)) { return parent::exists($name); @@ -132,11 +120,8 @@ public function exists($name) /** * Internal method that checks if the template name matches * the loaded object, with fallback support to partials. - * - * @param string $name The template name to validate - * @return bool */ - protected function validateCmsObject($name) + protected function validateCmsObject(string $name): bool { if ($this->obj && $name === $this->obj->getFilePath()) { return true; diff --git a/modules/cms/twig/PageNode.php b/modules/cms/twig/PageNode.php index f4481c9185..3dfb7173b5 100644 --- a/modules/cms/twig/PageNode.php +++ b/modules/cms/twig/PageNode.php @@ -25,7 +25,7 @@ public function compile(TwigCompiler $compiler) { $compiler ->addDebugInfo($this) - ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->pageFunction();\n") + ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->pageFunction(\$context);\n") ; } } diff --git a/modules/cms/twig/PartialNode.php b/modules/cms/twig/PartialNode.php index 34578dec7f..e1f1de0950 100644 --- a/modules/cms/twig/PartialNode.php +++ b/modules/cms/twig/PartialNode.php @@ -34,7 +34,7 @@ public function compile(TwigCompiler $compiler) } $compiler - ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->partialFunction(") + ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->partialFunction(\$context,") ->subcompile($this->getNode('nodes')->getNode(0)) ->write(", \$context['__cms_partial_params']") ->write(", true") diff --git a/modules/cms/twig/ScriptsNode.php b/modules/cms/twig/ScriptsNode.php index 4b2b9cdf50..d659440f39 100644 --- a/modules/cms/twig/ScriptsNode.php +++ b/modules/cms/twig/ScriptsNode.php @@ -25,7 +25,7 @@ public function compile(TwigCompiler $compiler) { $compiler ->addDebugInfo($this) - ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->assetsFunction('js');\n") + ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->assetsFunction(\$context, 'js');\n") ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->displayBlock('scripts');\n") ; } diff --git a/modules/cms/twig/StylesNode.php b/modules/cms/twig/StylesNode.php index 35a442c2f5..7ed22d47b3 100644 --- a/modules/cms/twig/StylesNode.php +++ b/modules/cms/twig/StylesNode.php @@ -25,7 +25,7 @@ public function compile(TwigCompiler $compiler) { $compiler ->addDebugInfo($this) - ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->assetsFunction('css');\n") + ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->assetsFunction(\$context, 'css');\n") ->write("echo \$this->env->getExtension('Cms\Twig\Extension')->displayBlock('styles');\n") ; } diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php index 3899141336..28aa72b1e1 100644 --- a/modules/system/ServiceProvider.php +++ b/modules/system/ServiceProvider.php @@ -12,8 +12,6 @@ use BackendAuth; use SystemException; use Backend\Models\UserRole; -use Twig\Extension\SandboxExtension; -use Twig\Environment as TwigEnvironment; use System\Classes\MailManager; use System\Classes\ErrorHandler; use System\Classes\MarkupManager; @@ -21,9 +19,6 @@ use System\Classes\SettingsManager; use System\Classes\UpdateManager; use System\Twig\Engine as TwigEngine; -use System\Twig\Loader as TwigLoader; -use System\Twig\Extension as TwigExtension; -use System\Twig\SecurityPolicy as TwigSecurityPolicy; use System\Models\EventLog; use System\Models\MailSetting; use System\Classes\CombineAssets; @@ -301,23 +296,22 @@ protected function registerLogging() } /* - * Register text twig parser + * Register Twig Environments and other Twig modifications provided by the module */ protected function registerTwigParser() { - /* - * Register system Twig environment - */ + // Register System Twig environment App::singleton('twig.environment', function ($app) { - $twig = new TwigEnvironment(new TwigLoader, ['auto_reload' => true]); - $twig->addExtension(new TwigExtension); - $twig->addExtension(new SandboxExtension(new TwigSecurityPolicy, true)); - return $twig; + return MarkupManager::makeBaseTwigEnvironment(); }); - /* - * Register .htm extension for Twig views - */ + // Register Mailer Twig environment + App::singleton('twig.environment.mailer', function ($app) { + $twig = MarkupManager::makeBaseTwigEnvironment(); + $twig->addTokenParser(new \System\Twig\MailPartialTokenParser); + }); + + // Register .htm extension for Twig views App::make('view')->addExtension('htm', 'twig', function () { return new TwigEngine(App::make('twig.environment')); }); diff --git a/modules/system/classes/MailManager.php b/modules/system/classes/MailManager.php index a166e6525e..af1877536c 100644 --- a/modules/system/classes/MailManager.php +++ b/modules/system/classes/MailManager.php @@ -1,12 +1,10 @@ startTwig(); - /* * Inject global view variables */ @@ -133,7 +121,7 @@ protected function addContentToMailerInternal($message, $template, $data, $plain $symfonyMessage = $message->getSymfonyMessage(); if (empty($symfonyMessage->getSubject())) { - $message->subject(Twig::parse($template->subject, $data)); + $message->subject($this->renderTwig($template->subject, $data)); } $data += [ @@ -155,11 +143,6 @@ protected function addContentToMailerInternal($message, $template, $data, $plain $text = $this->renderTextTemplate($template, $data); $message->addPart($text, 'text/plain'); - - /* - * End twig transaction - */ - $this->stopTwig(); } // @@ -202,7 +185,7 @@ public function renderTemplate($template, $data = []) $html = $this->renderTwig($template->layout->content_html, [ 'content' => $html, 'css' => $template->layout->content_css, - 'brandCss' => $css + 'brandCss' => $css, ] + (array) $data); $css .= PHP_EOL . $template->layout->content_css; @@ -276,56 +259,13 @@ public function renderPartial($code, array $params = []) } /** - * Internal helper for rendering Twig - */ - protected function renderTwig($content, $data = []) - { - if ($this->isTwigStarted) { - return Twig::parse($content, $data); - } - - $this->startTwig(); - - $result = Twig::parse($content, $data); - - $this->stopTwig(); - - return $result; - } - - /** - * Temporarily registers mail based token parsers with Twig. - * @return void - */ - protected function startTwig() - { - if ($this->isTwigStarted) { - return; - } - - $this->isTwigStarted = true; - - $markupManager = MarkupManager::instance(); - $markupManager->beginTransaction(); - $markupManager->registerTokenParsers([ - new MailPartialTokenParser - ]); - } - - /** - * Indicates that we are finished with Twig. - * @return void + * Internal helper for rendering Twig using the mailer Twig environment */ - protected function stopTwig() + protected function renderTwig(string $content, array $data = []): string { - if (!$this->isTwigStarted) { - return; - } - - $markupManager = MarkupManager::instance(); - $markupManager->endTransaction(); - - $this->isTwigStarted = false; + return App::make('twig.environment.mailer') + ->createTemplate($contents) + ->render($data); } // diff --git a/modules/system/classes/MarkupManager.php b/modules/system/classes/MarkupManager.php index 01f28c689e..95831351b4 100644 --- a/modules/system/classes/MarkupManager.php +++ b/modules/system/classes/MarkupManager.php @@ -4,6 +4,13 @@ use Twig\TokenParser\AbstractTokenParser as TwigTokenParser; use Twig\TwigFilter as TwigSimpleFilter; use Twig\TwigFunction as TwigSimpleFunction; +use Twig\Environment as TwigEnvironment; +use Twig\Extension\SandboxExtension; +use Twig\Loader\LoaderInterface; +use System\Twig\Loader as SystemTwigLoader; +use System\Twig\Extension as SystemTwigExtension; +use System\Twig\SecurityPolicy as TwigSecurityPolicy; + use ApplicationException; /** @@ -35,16 +42,6 @@ class MarkupManager */ protected $pluginManager; - /** - * @var array Transaction based extension items - */ - protected $transactionItems; - - /** - * @var bool Manager is in transaction mode - */ - protected $transactionMode = false; - /** * Initialize this singleton. */ @@ -53,18 +50,36 @@ protected function init() $this->pluginManager = PluginManager::instance(); } - protected function loadExtensions() + /** + * Make an instance of the base TwigEnvironment to extend further + */ + public static function makeBaseTwigEnvironment(LoaderInterface $loader = null, array $options = []): TwigEnvironment { - /* - * Load module items - */ + if (!$loader) { + $loader = new SystemTwigLoader(); + } + + $options = array_merge([ + 'auto_reload' => true, + ], $options); + + $twig = new TwigEnvironment($loader, $options); + $twig->addExtension(new SystemTwigExtension); + $twig->addExtension(new SandboxExtension(new TwigSecurityPolicy, true)); + return $twig; + } + + /** + * Loads all of the registered Twig extensions + */ + protected function loadExtensions(): void + { + // Load Module extensions foreach ($this->callbacks as $callback) { $callback($this); } - /* - * Load plugin items - */ + // Load Plugin extensions $plugins = $this->pluginManager->getPlugins(); foreach ($plugins as $id => $plugin) { @@ -95,69 +110,60 @@ protected function loadExtensions() * $manager->registerTokenParsers([...]); * }); * - * @param callable $callback A callable function. */ - public function registerCallback(callable $callback) + public function registerCallback(callable $callback): void { $this->callbacks[] = $callback; } /** - * Registers the CMS Twig extension items. - * The argument is an array of the extension definitions. The array keys represent the - * function/filter name, specific for the plugin/module. Each element in the - * array should be an associative array. - * @param string $type The extension type: filters, functions, tokens - * @param array $definitions An array of the extension definitions. + * Registers the Twig extension items. + * $type must be one of self::EXTENSION_TOKEN_PARSER, self::EXTENSION_FILTER, or self::EXTENSION_FUNCTION + * $definitions is of the format of [$extensionName => $associativeExtensionOptions] */ - public function registerExtensions($type, array $definitions) + public function registerExtensions(string $type, array $definitions): void { - $items = $this->transactionMode ? 'transactionItems' : 'items'; - - if ($this->$items === null) { - $this->$items = []; + if ($this->items === null) { + $this->items = []; } - if (!array_key_exists($type, $this->$items)) { - $this->$items[$type] = []; + if (!array_key_exists($type, $this->items)) { + $this->items[$type] = []; } foreach ($definitions as $name => $definition) { switch ($type) { case self::EXTENSION_TOKEN_PARSER: - $this->$items[$type][] = $definition; + $this->items[$type][] = $definition; break; case self::EXTENSION_FILTER: case self::EXTENSION_FUNCTION: - $this->$items[$type][$name] = $definition; + $this->items[$type][$name] = $definition; break; } } } /** - * Registers a CMS Twig Filter - * @param array $definitions An array of the extension definitions. + * Registers a Twig Filter */ - public function registerFilters(array $definitions) + public function registerFilters(array $definitions): void { $this->registerExtensions(self::EXTENSION_FILTER, $definitions); } /** - * Registers a CMS Twig Function - * @param array $definitions An array of the extension definitions. + * Registers a Twig Function */ - public function registerFunctions(array $definitions) + public function registerFunctions(array $definitions): void { $this->registerExtensions(self::EXTENSION_FUNCTION, $definitions); } /** - * Registers a CMS Twig Token Parser - * @param array $definitions An array of the extension definitions. + * Registers a Twig Token Parser */ - public function registerTokenParsers(array $definitions) + public function registerTokenParsers(array $definitions): void { $this->registerExtensions(self::EXTENSION_TOKEN_PARSER, $definitions); } @@ -179,10 +185,6 @@ public function listExtensions($type) $results = $this->items[$type]; } - if ($this->transactionItems !== null && isset($this->transactionItems[$type])) { - $results = array_merge($results, $this->transactionItems[$type]); - } - return $results; } @@ -373,41 +375,4 @@ protected function isWildCallable($callable, $replaceWith = false) return $isWild; } - - // - // Transactions - // - - /** - * Execute a single serving transaction, containing filters, functions, - * and token parsers that are disposed of afterwards. - * @param \Closure $callback - * @return void - */ - public function transaction(Closure $callback) - { - $this->beginTransaction(); - $callback($this); - $this->endTransaction(); - } - - /** - * Start a new transaction. - * @return void - */ - public function beginTransaction() - { - $this->transactionMode = true; - } - - /** - * Ends an active transaction. - * @return void - */ - public function endTransaction() - { - $this->transactionMode = false; - - $this->transactionItems = null; - } } diff --git a/modules/system/traits/AssetMaker.php b/modules/system/traits/AssetMaker.php index 5781441ed5..7668d67f0f 100644 --- a/modules/system/traits/AssetMaker.php +++ b/modules/system/traits/AssetMaker.php @@ -39,11 +39,10 @@ public function flushAssets() } /** - * Outputs `` and `