From 58afa17fd70ce5a2db26913aa2adb1c853b37de0 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Feb 2026 22:16:58 -0500 Subject: [PATCH 1/7] subset --- src/View/Cascade.php | 40 +++++++++++++++++++++++++++++++++++++- tests/View/CascadeTest.php | 33 ++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/View/Cascade.php b/src/View/Cascade.php index d8243897eed..4ab75505ed4 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -175,6 +175,44 @@ protected function hydrateContent() return $this; } + public static function config(): array + { + $defaultAllowlist = [ + 'app.name', + 'app.env', + 'app.debug', + 'app.url', + 'app.asset_url', + 'app.locale', + 'app.fallback_locale', + 'app.timezone', + 'auth.defaults', + 'auth.guards', + 'auth.passwords', + 'broadcasting.default', + 'cache.default', + 'filesystems.default', + 'mail.default', + 'mail.from', + 'queue.default', + 'session.lifetime', + 'session.expire_on_close', + 'session.driver', + 'statamic', + ]; + $config = []; + + foreach ((array) config('statamic.system.view_config_allowlist', $defaultAllowlist) as $key) { + $value = config($key); + + if (! is_null($value)) { + Arr::set($config, $key, $value); + } + } + + return $config; + } + private function contextualVariables() { return [ @@ -183,7 +221,7 @@ private function contextualVariables() 'xml_header' => '', // @TODO remove and document new best practice 'csrf_token' => csrf_token(), 'csrf_field' => csrf_field(), - 'config' => config()->all(), + 'config' => static::config(), 'response_code' => 200, // Auth diff --git a/tests/View/CascadeTest.php b/tests/View/CascadeTest.php index b5700be8301..5d8a53d6de6 100644 --- a/tests/View/CascadeTest.php +++ b/tests/View/CascadeTest.php @@ -84,13 +84,44 @@ public function it_hydrates_constants() $this->assertEquals('', $cascade['xml_header']); $this->assertEquals(csrf_token(), $cascade['csrf_token']); $this->assertEquals(csrf_field(), $cascade['csrf_field']); - $this->assertEquals(config()->all(), $cascade['config']); + $this->assertEquals(Cascade::config(), $cascade['config']); // Response code is constant. It gets manually overridden on errors. $this->assertEquals(200, $cascade['response_code']); }); } + #[Test] + public function it_only_hydrates_allowlisted_config_values() + { + config([ + 'app.foo' => 'bar', + 'statamic.system.view_config_allowlist' => ['app.name'], + ]); + + tap($this->cascade()->hydrate()->toArray(), function ($cascade) { + $this->assertTrue(Arr::has($cascade['config'], 'app.name')); + $this->assertFalse(Arr::has($cascade['config'], 'app.foo')); + }); + } + + #[Test] + public function overriding_the_allowlist_changes_the_config_subset() + { + config(['statamic.system.view_config_allowlist' => ['app.name']]); + + $nameOnly = Cascade::config(); + + config(['statamic.system.view_config_allowlist' => ['app.env']]); + + $envOnly = Cascade::config(); + + $this->assertTrue(Arr::has($nameOnly, 'app.name')); + $this->assertFalse(Arr::has($nameOnly, 'app.env')); + $this->assertTrue(Arr::has($envOnly, 'app.env')); + $this->assertFalse(Arr::has($envOnly, 'app.name')); + } + #[Test] public function it_hydrates_auth_when_logged_in() { From c88f437613511f4ea529682f151f3fe5e264db60 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Feb 2026 22:32:16 -0500 Subject: [PATCH 2/7] spread --- src/View/Cascade.php | 9 ++++++++- tests/View/CascadeTest.php | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/View/Cascade.php b/src/View/Cascade.php index 4ab75505ed4..d25bb9317bb 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -200,9 +200,16 @@ public static function config(): array 'session.driver', 'statamic', ]; + $allowlist = (array) config('statamic.system.view_config_allowlist', $defaultAllowlist); + + if (($index = array_search('@default', $allowlist)) !== false) { + array_splice($allowlist, $index, 1, $defaultAllowlist); + $allowlist = array_values(array_unique($allowlist)); + } + $config = []; - foreach ((array) config('statamic.system.view_config_allowlist', $defaultAllowlist) as $key) { + foreach ($allowlist as $key) { $value = config($key); if (! is_null($value)) { diff --git a/tests/View/CascadeTest.php b/tests/View/CascadeTest.php index 5d8a53d6de6..9cb970d64de 100644 --- a/tests/View/CascadeTest.php +++ b/tests/View/CascadeTest.php @@ -122,6 +122,20 @@ public function overriding_the_allowlist_changes_the_config_subset() $this->assertFalse(Arr::has($envOnly, 'app.name')); } + #[Test] + public function default_allowlist_can_be_extended_with_default_spread_syntax() + { + config([ + 'app.foo' => 'bar', + 'statamic.system.view_config_allowlist' => ['@default', 'app.foo'], + ]); + + $config = Cascade::config(); + + $this->assertTrue(Arr::has($config, 'app.name')); + $this->assertTrue(Arr::has($config, 'app.foo')); + } + #[Test] public function it_hydrates_auth_when_logged_in() { From e76ef57ca9773edfb5913d1f05414931e83a4afa Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Feb 2026 22:39:59 -0500 Subject: [PATCH 3/7] style --- src/View/Cascade.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/View/Cascade.php b/src/View/Cascade.php index d25bb9317bb..f037e1e86cf 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -177,7 +177,7 @@ protected function hydrateContent() public static function config(): array { - $defaultAllowlist = [ + $defaults = [ 'app.name', 'app.env', 'app.debug', @@ -200,24 +200,20 @@ public static function config(): array 'session.driver', 'statamic', ]; - $allowlist = (array) config('statamic.system.view_config_allowlist', $defaultAllowlist); - if (($index = array_search('@default', $allowlist)) !== false) { - array_splice($allowlist, $index, 1, $defaultAllowlist); - $allowlist = array_values(array_unique($allowlist)); - } - - $config = []; + $allowed = collect((array) config('statamic.system.view_config_allowlist', $defaults)) + ->flatMap(fn ($key) => $key === '@default' ? $defaults : [$key]) + ->unique()->values()->all(); - foreach ($allowlist as $key) { + return array_reduce($allowed, function ($config, $key) { $value = config($key); if (! is_null($value)) { Arr::set($config, $key, $value); } - } - return $config; + return $config; + }, []); } private function contextualVariables() From 5c609f1927fd0ea05c952560660bba719411cabd Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Feb 2026 22:52:42 -0500 Subject: [PATCH 4/7] dont include all of statamic --- src/View/Cascade.php | 98 +++++++++++++++++++++++++++++++++++++- tests/View/CascadeTest.php | 2 + 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/View/Cascade.php b/src/View/Cascade.php index f037e1e86cf..8e463e3a69e 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -198,7 +198,103 @@ public static function config(): array 'session.lifetime', 'session.expire_on_close', 'session.driver', - 'statamic', + 'statamic.assets.image_manipulation', + 'statamic.assets.auto_crop', + 'statamic.assets.thumbnails', + 'statamic.assets.video_thumbnails', + 'statamic.assets.google_docs_viewer', + 'statamic.assets.cache_meta', + 'statamic.assets.focal_point_editor', + 'statamic.assets.lowercase', + 'statamic.assets.svg_sanitization_on_upload', + 'statamic.assets.ffmpeg', + 'statamic.assets.set_preview_images', + 'statamic.autosave', + 'statamic.cp', + 'statamic.editions', + 'statamic.forms.email_view_folder', + 'statamic.forms.send_email_job', + 'statamic.forms.exporters', + 'statamic.git.enabled', + 'statamic.git.automatic', + 'statamic.git.queue_connection', + 'statamic.git.dispatch_delay', + 'statamic.git.use_authenticated', + 'statamic.git.user', + 'statamic.git.binary', + 'statamic.git.commands', + 'statamic.git.push', + 'statamic.git.ignored_events', + 'statamic.git.locale', + 'statamic.graphql', + 'statamic.live_preview', + 'statamic.markdown', + 'statamic.oauth', + 'statamic.protect.default', + 'statamic.revisions', + 'statamic.routes', + 'statamic.search.default', + 'statamic.search.indexes', + 'statamic.search.defaults', + 'statamic.search.queue', + 'statamic.search.queue_connection', + 'statamic.search.chunk_size', + 'statamic.stache.watcher', + 'statamic.stache.cache_store', + 'statamic.stache.indexes', + 'statamic.stache.lock', + 'statamic.stache.warming', + 'statamic.static_caching.strategy', + 'statamic.static_caching.strategies', + 'statamic.static_caching.exclude', + 'statamic.static_caching.invalidation', + 'statamic.static_caching.ignore_query_strings', + 'statamic.static_caching.allowed_query_strings', + 'statamic.static_caching.disallowed_query_strings', + 'statamic.static_caching.nocache', + 'statamic.static_caching.nocache_db_connection', + 'statamic.static_caching.replacers', + 'statamic.static_caching.warm_queue', + 'statamic.static_caching.warm_queue_connection', + 'statamic.static_caching.warm_insecure', + 'statamic.static_caching.background_recache', + 'statamic.static_caching.recache_token_parameter', + 'statamic.static_caching.share_errors', + 'statamic.system.multisite', + 'statamic.system.send_powered_by_header', + 'statamic.system.date_format', + 'statamic.system.display_timezone', + 'statamic.system.localize_dates_in_modifiers', + 'statamic.system.charset', + 'statamic.system.track_last_update', + 'statamic.system.cache_tags_enabled', + 'statamic.system.php_memory_limit', + 'statamic.system.php_max_execution_time', + 'statamic.system.ajax_timeout', + 'statamic.system.pcre_backtrack_limit', + 'statamic.system.debugbar', + 'statamic.system.ascii_replace_extra_symbols', + 'statamic.system.update_references', + 'statamic.system.always_augment_to_query', + 'statamic.system.row_id_handle', + 'statamic.system.fake_sql_queries', + 'statamic.system.layout', + 'statamic.templates', + 'statamic.users.repository', + 'statamic.users.avatars', + 'statamic.users.new_user_roles', + 'statamic.users.new_user_groups', + 'statamic.users.wizard_invitation', + 'statamic.users.passwords', + 'statamic.users.database', + 'statamic.users.tables', + 'statamic.users.guards', + 'statamic.users.impersonate', + 'statamic.users.elevated_session_duration', + 'statamic.users.two_factor_enforced_roles', + 'statamic.users.sort_field', + 'statamic.users.sort_direction', + 'statamic.webauthn', ]; $allowed = collect((array) config('statamic.system.view_config_allowlist', $defaults)) diff --git a/tests/View/CascadeTest.php b/tests/View/CascadeTest.php index 9cb970d64de..fe8f0a6d96c 100644 --- a/tests/View/CascadeTest.php +++ b/tests/View/CascadeTest.php @@ -127,6 +127,7 @@ public function default_allowlist_can_be_extended_with_default_spread_syntax() { config([ 'app.foo' => 'bar', + 'statamic.system.license_key' => 'test-license-key', 'statamic.system.view_config_allowlist' => ['@default', 'app.foo'], ]); @@ -134,6 +135,7 @@ public function default_allowlist_can_be_extended_with_default_spread_syntax() $this->assertTrue(Arr::has($config, 'app.name')); $this->assertTrue(Arr::has($config, 'app.foo')); + $this->assertFalse(Arr::has($config, 'statamic.system.license_key')); } #[Test] From 5cef4ca61724c7010cbef49e111f6e067921fa56 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Feb 2026 22:53:39 -0500 Subject: [PATCH 5/7] move to end --- src/View/Cascade.php | 146 +++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/View/Cascade.php b/src/View/Cascade.php index 8e463e3a69e..9d5bd46d102 100644 --- a/src/View/Cascade.php +++ b/src/View/Cascade.php @@ -175,6 +175,79 @@ protected function hydrateContent() return $this; } + private function contextualVariables() + { + return [ + // Constants + 'environment' => app()->environment(), + 'xml_header' => '', // @TODO remove and document new best practice + 'csrf_token' => csrf_token(), + 'csrf_field' => csrf_field(), + 'config' => static::config(), + 'response_code' => 200, + + // Auth + 'logged_in' => $loggedIn = auth(config('statamic.users.guards.web', 'web'))->check(), + 'logged_out' => ! $loggedIn, + 'current_user' => User::current(), + + // Date + 'current_date' => $now = now(), + 'now' => $now, + 'today' => $now, + + // Request + 'current_url' => $this->request->url(), + 'current_full_url' => $this->request->fullUrl(), + 'current_uri' => URL::tidy($this->request->path()), + 'get_post' => Arr::sanitize($this->request->all()), + 'get' => Arr::sanitize($this->request->query->all()), + 'post' => $this->request->isMethod('post') ? Arr::sanitize($this->request->request->all()) : [], + 'old' => Arr::sanitize(old(null, [])), + + 'site' => $this->site, + 'sites' => Facades\Site::all()->values(), + 'homepage' => $this->site->url(), + 'is_homepage' => $this->site->absoluteUrl() == $this->request->url(), + 'cp_url' => cp_route('index'), + ]; + } + + protected function hydrateViewModel() + { + if ($class = optional($this->get('view_model'))->value()) { + $viewModel = new $class($this); + $this->data = array_merge($this->data, $viewModel->data()); + } + + return $this; + } + + public function getViewData($view) + { + $all = $this->get('views') ?? []; + + return collect($all) + ->reverse() + ->reduce(function ($carry, $data) { + return $carry->merge($data); + }, collect()) + ->merge($all[$view]) + ->all(); + } + + public function sections() + { + return $this->sections; + } + + public function clearSections() + { + $this->sections = collect(); + + return $this; + } + public static function config(): array { $defaults = [ @@ -311,77 +384,4 @@ public static function config(): array return $config; }, []); } - - private function contextualVariables() - { - return [ - // Constants - 'environment' => app()->environment(), - 'xml_header' => '', // @TODO remove and document new best practice - 'csrf_token' => csrf_token(), - 'csrf_field' => csrf_field(), - 'config' => static::config(), - 'response_code' => 200, - - // Auth - 'logged_in' => $loggedIn = auth(config('statamic.users.guards.web', 'web'))->check(), - 'logged_out' => ! $loggedIn, - 'current_user' => User::current(), - - // Date - 'current_date' => $now = now(), - 'now' => $now, - 'today' => $now, - - // Request - 'current_url' => $this->request->url(), - 'current_full_url' => $this->request->fullUrl(), - 'current_uri' => URL::tidy($this->request->path()), - 'get_post' => Arr::sanitize($this->request->all()), - 'get' => Arr::sanitize($this->request->query->all()), - 'post' => $this->request->isMethod('post') ? Arr::sanitize($this->request->request->all()) : [], - 'old' => Arr::sanitize(old(null, [])), - - 'site' => $this->site, - 'sites' => Facades\Site::all()->values(), - 'homepage' => $this->site->url(), - 'is_homepage' => $this->site->absoluteUrl() == $this->request->url(), - 'cp_url' => cp_route('index'), - ]; - } - - protected function hydrateViewModel() - { - if ($class = optional($this->get('view_model'))->value()) { - $viewModel = new $class($this); - $this->data = array_merge($this->data, $viewModel->data()); - } - - return $this; - } - - public function getViewData($view) - { - $all = $this->get('views') ?? []; - - return collect($all) - ->reverse() - ->reduce(function ($carry, $data) { - return $carry->merge($data); - }, collect()) - ->merge($all[$view]) - ->all(); - } - - public function sections() - { - return $this->sections; - } - - public function clearSections() - { - $this->sections = collect(); - - return $this; - } } From 81851decc625f3d424f8d4c0749e68ded6fb7502 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Feb 2026 23:05:35 -0500 Subject: [PATCH 6/7] replace uses of config all --- src/Addons/Settings.php | 3 ++- src/Entries/Entry.php | 3 ++- src/Forms/Email.php | 5 +++-- src/Sites/Site.php | 3 ++- tests/Addons/SettingsTest.php | 10 ++++++++-- tests/Sites/SitesConfigTest.php | 1 + 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Addons/Settings.php b/src/Addons/Settings.php index a079cb14f17..2f30325d29f 100644 --- a/src/Addons/Settings.php +++ b/src/Addons/Settings.php @@ -8,6 +8,7 @@ use Statamic\Events\AddonSettingsSaving; use Statamic\Facades\Antlers; use Statamic\Support\Arr; +use Statamic\View\Cascade; abstract class Settings implements Contract { @@ -96,6 +97,6 @@ protected function resolveAntlersValue($value) ->all(); } - return (string) Antlers::parse($value, ['config' => config()->all()]); + return (string) Antlers::parse($value, ['config' => Cascade::config()]); } } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index a36d654ce92..cac3dfa1c53 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -52,6 +52,7 @@ use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; +use Statamic\View\Cascade; class Entry implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract { @@ -1106,7 +1107,7 @@ private function resolvePreviewTargetUrl($format) } return (string) Antlers::parse($format, array_merge($this->routeData(), [ - 'config' => config()->all(), + 'config' => Cascade::config(), 'site' => $this->site(), 'uri' => $this->uri(), 'url' => $this->url(), diff --git a/src/Forms/Email.php b/src/Forms/Email.php index 55a4320f597..076bc20b279 100644 --- a/src/Forms/Email.php +++ b/src/Forms/Email.php @@ -15,6 +15,7 @@ use Statamic\Sites\Site; use Statamic\Support\Arr; use Statamic\Support\Str; +use Statamic\View\Cascade; use function Statamic\trans as __; @@ -170,7 +171,7 @@ protected function addData() $data = array_merge($augmented, $this->getGlobalsData(), [ 'form_config' => $formConfig, 'email_config' => $this->config, - 'config' => config()->all(), + 'config' => Cascade::config(), 'fields' => $fields, 'site_url' => Config::getSiteUrl(), 'date' => now(), @@ -245,7 +246,7 @@ protected function parseConfig(array $config) $value = Parse::env($value); // deprecated return (string) Antlers::parse($value, array_merge( - ['config' => config()->all()], + ['config' => Cascade::config()], $this->getGlobalsData(), $this->submissionData, )); diff --git a/src/Sites/Site.php b/src/Sites/Site.php index a11bab0ef8d..64abf2e7ba8 100644 --- a/src/Sites/Site.php +++ b/src/Sites/Site.php @@ -9,6 +9,7 @@ use Statamic\Support\Str; use Statamic\Support\TextDirection; use Statamic\View\Antlers\Language\Runtime\RuntimeParser; +use Statamic\View\Cascade; class Site implements Augmentable { @@ -116,7 +117,7 @@ protected function resolveAntlersValue($value) ->all(); } - return (string) app(RuntimeParser::class)->parse($value, ['config' => config()->all()]); + return (string) app(RuntimeParser::class)->parse($value, ['config' => Cascade::config()]); } private function removePath($url) diff --git a/tests/Addons/SettingsTest.php b/tests/Addons/SettingsTest.php index da57fd17c8b..2621db2d521 100644 --- a/tests/Addons/SettingsTest.php +++ b/tests/Addons/SettingsTest.php @@ -93,7 +93,10 @@ public function it_gets_a_dotted_value() #[Test] public function it_sets_a_value() { - config(['test' => ['a' => 'A', 'b' => 'B']]); + config([ + 'test' => ['a' => 'A', 'b' => 'B'], + 'statamic.system.view_config_allowlist' => ['@default', 'test.a', 'test.b'], + ]); $addon = $this->makeFromPackage(); $settings = new Settings($addon, ['foo' => 'bar']); @@ -139,7 +142,10 @@ public function it_sets_a_dotted_value() #[Test] public function it_sets_all_values() { - config(['test' => ['a' => 'A', 'b' => 'B']]); + config([ + 'test' => ['a' => 'A', 'b' => 'B'], + 'statamic.system.view_config_allowlist' => ['@default', 'test.a', 'test.b'], + ]); $addon = $this->makeFromPackage(); $settings = new Settings($addon, ['foo' => 'bar']); diff --git a/tests/Sites/SitesConfigTest.php b/tests/Sites/SitesConfigTest.php index 5419d31a2ad..c10f2cb73b8 100644 --- a/tests/Sites/SitesConfigTest.php +++ b/tests/Sites/SitesConfigTest.php @@ -133,6 +133,7 @@ public function it_resolves_antlers_when_resolving_sites() ]); Config::set('statamic.some_addon.theme', 'sunset'); + Config::set('statamic.system.view_config_allowlist', ['@default', 'app.faker_locale', 'statamic.some_addon.theme']); Site::setSites([ 'default' => [ From f8436e3e4ed540cb7b197a3f04338d6ec274ec74 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 24 Feb 2026 10:17:27 -0500 Subject: [PATCH 7/7] add mode to treat as user content and use it ... but not for the site since it happens during bootstrapping it causes an infinite loop. do it manually there. --- src/Addons/Settings.php | 2 +- src/Entries/Entry.php | 4 +- src/Facades/Antlers.php | 1 + src/Forms/Email.php | 2 +- src/Sites/Site.php | 10 ++- src/View/Antlers/Antlers.php | 13 ++++ tests/Antlers/ParseUserContentTest.php | 91 ++++++++++++++++++++++++++ 7 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 tests/Antlers/ParseUserContentTest.php diff --git a/src/Addons/Settings.php b/src/Addons/Settings.php index 2f30325d29f..ef283122690 100644 --- a/src/Addons/Settings.php +++ b/src/Addons/Settings.php @@ -97,6 +97,6 @@ protected function resolveAntlersValue($value) ->all(); } - return (string) Antlers::parse($value, ['config' => Cascade::config()]); + return (string) Antlers::parseUserContent($value, ['config' => Cascade::config()]); } } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index cac3dfa1c53..52289236bfa 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -1082,7 +1082,7 @@ public function autoGeneratedTitle() // Since the slug is generated from the title, we'll avoid augmenting // the slug which could result in an infinite loop in some cases. - $title = $this->withLocale($this->site()->lang(), fn () => (string) Antlers::parse($format, $this->augmented()->except('slug')->all())); + $title = $this->withLocale($this->site()->lang(), fn () => (string) Antlers::parseUserContent($format, $this->augmented()->except('slug')->all())); return trim($title); } @@ -1106,7 +1106,7 @@ private function resolvePreviewTargetUrl($format) }, $format); } - return (string) Antlers::parse($format, array_merge($this->routeData(), [ + return (string) Antlers::parseUserContent($format, array_merge($this->routeData(), [ 'config' => Cascade::config(), 'site' => $this->site(), 'uri' => $this->uri(), diff --git a/src/Facades/Antlers.php b/src/Facades/Antlers.php index 565291af83d..e29d69889bd 100644 --- a/src/Facades/Antlers.php +++ b/src/Facades/Antlers.php @@ -10,6 +10,7 @@ * @method static Parser parser() * @method static mixed usingParser(Parser $parser, \Closure $callback) * @method static AntlersString parse(string $str, array $variables = []) + * @method static AntlersString parseUserContent(string $str, array $variables = []) * @method static string parseLoop(string $content, array $data, bool $supplement = true, array $context = []) * @method static array identifiers(string $content) * diff --git a/src/Forms/Email.php b/src/Forms/Email.php index 076bc20b279..0ce40bf5671 100644 --- a/src/Forms/Email.php +++ b/src/Forms/Email.php @@ -245,7 +245,7 @@ protected function parseConfig(array $config) return collect($config)->map(function ($value) { $value = Parse::env($value); // deprecated - return (string) Antlers::parse($value, array_merge( + return (string) Antlers::parseUserContent($value, array_merge( ['config' => Cascade::config()], $this->getGlobalsData(), $this->submissionData, diff --git a/src/Sites/Site.php b/src/Sites/Site.php index 64abf2e7ba8..f895c0d3616 100644 --- a/src/Sites/Site.php +++ b/src/Sites/Site.php @@ -8,6 +8,7 @@ use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Support\TextDirection; +use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState; use Statamic\View\Antlers\Language\Runtime\RuntimeParser; use Statamic\View\Cascade; @@ -117,7 +118,14 @@ protected function resolveAntlersValue($value) ->all(); } - return (string) app(RuntimeParser::class)->parse($value, ['config' => Cascade::config()]); + $isEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData; + GlobalRuntimeState::$isEvaluatingUserData = true; + + try { + return (string) app(RuntimeParser::class)->parse($value, ['config' => Cascade::config()]); + } finally { + GlobalRuntimeState::$isEvaluatingUserData = $isEvaluatingUserData; + } } private function removePath($url) diff --git a/src/View/Antlers/Antlers.php b/src/View/Antlers/Antlers.php index b0fc4f9c43a..b1dc611cfbc 100644 --- a/src/View/Antlers/Antlers.php +++ b/src/View/Antlers/Antlers.php @@ -5,6 +5,7 @@ use Closure; use Statamic\Contracts\View\Antlers\Parser; use Statamic\View\Antlers\Language\Parser\IdentifierFinder; +use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState; class Antlers { @@ -31,6 +32,18 @@ public function parse($str, $variables = []) return $this->parser()->parse($str, $variables); } + public function parseUserContent($str, $variables = []) + { + $isEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData; + GlobalRuntimeState::$isEvaluatingUserData = true; + + try { + return $this->parser()->parse($str, $variables); + } finally { + GlobalRuntimeState::$isEvaluatingUserData = $isEvaluatingUserData; + } + } + /** * Iterate over an array and parse the string/template for each. * diff --git a/tests/Antlers/ParseUserContentTest.php b/tests/Antlers/ParseUserContentTest.php new file mode 100644 index 00000000000..5177ee45152 --- /dev/null +++ b/tests/Antlers/ParseUserContentTest.php @@ -0,0 +1,91 @@ +assertSame( + (string) Antlers::parse('Hello {{ name }}!', ['name' => 'Jason']), + (string) Antlers::parseUserContent('Hello {{ name }}!', ['name' => 'Jason']) + ); + } + + #[Test] + public function it_blocks_php_nodes_in_user_content_mode() + { + Log::shouldReceive('warning') + ->once() + ->with('PHP Node evaluated in user content: {{? echo Str::upper(\'hello\') ?}}', \Mockery::type('array')); + + $result = (string) Antlers::parseUserContent('Text: {{? echo Str::upper(\'hello\') ?}}'); + + $this->assertSame('Text: ', $result); + } + + #[Test] + public function it_blocks_method_calls_in_user_content_mode() + { + Log::shouldReceive('warning') + ->once() + ->with('Method call evaluated in user content.', \Mockery::type('array')); + + $result = (string) Antlers::parseUserContent('{{ object:method("hello") }}', [ + 'object' => new ClassOne(), + ]); + + $this->assertSame('', $result); + } + + #[Test] + public function it_restores_user_data_flag_after_successful_parse() + { + GlobalRuntimeState::$isEvaluatingUserData = false; + + Antlers::parseUserContent('Hello {{ name }}!', ['name' => 'Jason']); + + $this->assertFalse(GlobalRuntimeState::$isEvaluatingUserData); + } + + #[Test] + public function it_restores_user_data_flag_after_parse_exceptions() + { + GlobalRuntimeState::$isEvaluatingUserData = false; + $parser = \Mockery::mock(Parser::class); + $parser->shouldReceive('parse') + ->once() + ->andThrow(new \RuntimeException('Failed to parse user content.')); + + try { + Antlers::usingParser($parser, function ($antlers) { + $antlers->parseUserContent('Hello {{ name }}', ['name' => 'Jason']); + }); + + $this->fail('Expected RuntimeException to be thrown.'); + } catch (\RuntimeException $exception) { + $this->assertSame('Failed to parse user content.', $exception->getMessage()); + } + + $this->assertFalse(GlobalRuntimeState::$isEvaluatingUserData); + } +}