Skip to content

Commit 01ca084

Browse files
authored
[5.x] Antlers user content and config (#14058)
1 parent 7102b95 commit 01ca084

File tree

9 files changed

+310
-9
lines changed

9 files changed

+310
-9
lines changed

src/Entries/Entry.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use Statamic\Support\Arr;
5353
use Statamic\Support\Str;
5454
use Statamic\Support\Traits\FluentlyGetsAndSets;
55+
use Statamic\View\Cascade;
5556

5657
class Entry implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract
5758
{
@@ -1040,7 +1041,7 @@ public function autoGeneratedTitle()
10401041

10411042
// Since the slug is generated from the title, we'll avoid augmenting
10421043
// the slug which could result in an infinite loop in some cases.
1043-
$title = $this->withLocale($this->site()->lang(), fn () => (string) Antlers::parse($format, $this->augmented()->except('slug')->all()));
1044+
$title = $this->withLocale($this->site()->lang(), fn () => (string) Antlers::parseUserContent($format, $this->augmented()->except('slug')->all()));
10441045

10451046
return trim($title);
10461047
}
@@ -1064,8 +1065,8 @@ private function resolvePreviewTargetUrl($format)
10641065
}, $format);
10651066
}
10661067

1067-
return (string) Antlers::parse($format, array_merge($this->routeData(), [
1068-
'config' => config()->all(),
1068+
return (string) Antlers::parseUserContent($format, array_merge($this->routeData(), [
1069+
'config' => Cascade::config(),
10691070
'site' => $this->site(),
10701071
'uri' => $this->uri(),
10711072
'url' => $this->url(),

src/Facades/Antlers.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* @method static Parser parser()
1111
* @method static mixed usingParser(Parser $parser, \Closure $callback)
1212
* @method static AntlersString parse(string $str, array $variables = [])
13+
* @method static AntlersString parseUserContent(string $str, array $variables = [])
1314
* @method static string parseLoop(string $content, array $data, bool $supplement = true, array $context = [])
1415
* @method static array identifiers(string $content)
1516
*

src/Forms/Email.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Statamic\Sites\Site;
1616
use Statamic\Support\Arr;
1717
use Statamic\Support\Str;
18+
use Statamic\View\Cascade;
1819

1920
use function Statamic\trans as __;
2021

@@ -170,7 +171,7 @@ protected function addData()
170171
$data = array_merge($augmented, $this->getGlobalsData(), [
171172
'form_config' => $formConfig,
172173
'email_config' => $this->config,
173-
'config' => config()->all(),
174+
'config' => Cascade::config(),
174175
'fields' => $fields,
175176
'site_url' => Config::getSiteUrl(),
176177
'date' => now(),
@@ -244,8 +245,8 @@ protected function parseConfig(array $config)
244245
return collect($config)->map(function ($value) {
245246
$value = Parse::env($value); // deprecated
246247

247-
return (string) Antlers::parse($value, array_merge(
248-
['config' => config()->all()],
248+
return (string) Antlers::parseUserContent($value, array_merge(
249+
['config' => Cascade::config()],
249250
$this->getGlobalsData(),
250251
$this->submissionData,
251252
));

src/Sites/Site.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
use Statamic\Support\Arr;
88
use Statamic\Support\Str;
99
use Statamic\Support\TextDirection;
10+
use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState;
1011
use Statamic\View\Antlers\Language\Runtime\RuntimeParser;
12+
use Statamic\View\Cascade;
1113

1214
class Site implements Augmentable
1315
{
@@ -129,7 +131,14 @@ protected function resolveAntlersValue($value)
129131
->all();
130132
}
131133

132-
return (string) app(RuntimeParser::class)->parse($value, ['config' => config()->all()]);
134+
$isEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
135+
GlobalRuntimeState::$isEvaluatingUserData = true;
136+
137+
try {
138+
return (string) app(RuntimeParser::class)->parse($value, ['config' => Cascade::config()]);
139+
} finally {
140+
GlobalRuntimeState::$isEvaluatingUserData = $isEvaluatingUserData;
141+
}
133142
}
134143

135144
private function removePath($url)

src/View/Antlers/Antlers.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Closure;
66
use Statamic\Contracts\View\Antlers\Parser;
77
use Statamic\View\Antlers\Language\Parser\IdentifierFinder;
8+
use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState;
89

910
class Antlers
1011
{
@@ -31,6 +32,18 @@ public function parse($str, $variables = [])
3132
return $this->parser()->parse($str, $variables);
3233
}
3334

35+
public function parseUserContent($str, $variables = [])
36+
{
37+
$isEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
38+
GlobalRuntimeState::$isEvaluatingUserData = true;
39+
40+
try {
41+
return $this->parser()->parse($str, $variables);
42+
} finally {
43+
GlobalRuntimeState::$isEvaluatingUserData = $isEvaluatingUserData;
44+
}
45+
}
46+
3447
/**
3548
* Iterate over an array and parse the string/template for each.
3649
*

src/View/Cascade.php

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ private function contextualVariables()
183183
'xml_header' => '<?xml version="1.0" encoding="utf-8" ?>', // @TODO remove and document new best practice
184184
'csrf_token' => csrf_token(),
185185
'csrf_field' => csrf_field(),
186-
'config' => config()->all(),
186+
'config' => static::config(),
187187
'response_code' => 200,
188188

189189
// Auth
@@ -247,4 +247,141 @@ public function clearSections()
247247

248248
return $this;
249249
}
250+
251+
public static function config(): array
252+
{
253+
$defaults = [
254+
'app.name',
255+
'app.env',
256+
'app.debug',
257+
'app.url',
258+
'app.asset_url',
259+
'app.locale',
260+
'app.fallback_locale',
261+
'app.timezone',
262+
'auth.defaults',
263+
'auth.guards',
264+
'auth.passwords',
265+
'broadcasting.default',
266+
'cache.default',
267+
'filesystems.default',
268+
'mail.default',
269+
'mail.from',
270+
'queue.default',
271+
'session.lifetime',
272+
'session.expire_on_close',
273+
'session.driver',
274+
'statamic.assets.image_manipulation',
275+
'statamic.assets.auto_crop',
276+
'statamic.assets.thumbnails',
277+
'statamic.assets.video_thumbnails',
278+
'statamic.assets.google_docs_viewer',
279+
'statamic.assets.cache_meta',
280+
'statamic.assets.focal_point_editor',
281+
'statamic.assets.lowercase',
282+
'statamic.assets.svg_sanitization_on_upload',
283+
'statamic.assets.ffmpeg',
284+
'statamic.assets.set_preview_images',
285+
'statamic.autosave',
286+
'statamic.cp',
287+
'statamic.editions',
288+
'statamic.forms.email_view_folder',
289+
'statamic.forms.send_email_job',
290+
'statamic.forms.exporters',
291+
'statamic.git.enabled',
292+
'statamic.git.automatic',
293+
'statamic.git.queue_connection',
294+
'statamic.git.dispatch_delay',
295+
'statamic.git.use_authenticated',
296+
'statamic.git.user',
297+
'statamic.git.binary',
298+
'statamic.git.commands',
299+
'statamic.git.push',
300+
'statamic.git.ignored_events',
301+
'statamic.git.locale',
302+
'statamic.graphql',
303+
'statamic.live_preview',
304+
'statamic.markdown',
305+
'statamic.oauth',
306+
'statamic.protect.default',
307+
'statamic.revisions',
308+
'statamic.routes',
309+
'statamic.search.default',
310+
'statamic.search.indexes',
311+
'statamic.search.defaults',
312+
'statamic.search.queue',
313+
'statamic.search.queue_connection',
314+
'statamic.search.chunk_size',
315+
'statamic.stache.watcher',
316+
'statamic.stache.cache_store',
317+
'statamic.stache.indexes',
318+
'statamic.stache.lock',
319+
'statamic.stache.warming',
320+
'statamic.static_caching.strategy',
321+
'statamic.static_caching.strategies',
322+
'statamic.static_caching.exclude',
323+
'statamic.static_caching.invalidation',
324+
'statamic.static_caching.ignore_query_strings',
325+
'statamic.static_caching.allowed_query_strings',
326+
'statamic.static_caching.disallowed_query_strings',
327+
'statamic.static_caching.nocache',
328+
'statamic.static_caching.nocache_db_connection',
329+
'statamic.static_caching.replacers',
330+
'statamic.static_caching.warm_queue',
331+
'statamic.static_caching.warm_queue_connection',
332+
'statamic.static_caching.warm_insecure',
333+
'statamic.static_caching.background_recache',
334+
'statamic.static_caching.recache_token_parameter',
335+
'statamic.static_caching.share_errors',
336+
'statamic.system.multisite',
337+
'statamic.system.send_powered_by_header',
338+
'statamic.system.date_format',
339+
'statamic.system.display_timezone',
340+
'statamic.system.localize_dates_in_modifiers',
341+
'statamic.system.charset',
342+
'statamic.system.track_last_update',
343+
'statamic.system.cache_tags_enabled',
344+
'statamic.system.php_memory_limit',
345+
'statamic.system.php_max_execution_time',
346+
'statamic.system.ajax_timeout',
347+
'statamic.system.pcre_backtrack_limit',
348+
'statamic.system.debugbar',
349+
'statamic.system.ascii_replace_extra_symbols',
350+
'statamic.system.update_references',
351+
'statamic.system.always_augment_to_query',
352+
'statamic.system.row_id_handle',
353+
'statamic.system.fake_sql_queries',
354+
'statamic.system.layout',
355+
'statamic.templates',
356+
'statamic.users.repository',
357+
'statamic.users.avatars',
358+
'statamic.users.new_user_roles',
359+
'statamic.users.new_user_groups',
360+
'statamic.users.wizard_invitation',
361+
'statamic.users.passwords',
362+
'statamic.users.database',
363+
'statamic.users.tables',
364+
'statamic.users.guards',
365+
'statamic.users.impersonate',
366+
'statamic.users.elevated_session_duration',
367+
'statamic.users.two_factor_enforced_roles',
368+
'statamic.users.sort_field',
369+
'statamic.users.sort_direction',
370+
'statamic.webauthn',
371+
];
372+
373+
$allowed = collect((array) config('statamic.system.view_config_allowlist', $defaults))
374+
->flatMap(fn ($key) => $key === '@default' ? $defaults : [$key])
375+
->unique()->values()->all();
376+
377+
return array_reduce($allowed, function ($config, $key) {
378+
$value = config($key);
379+
380+
if (! is_null($value)) {
381+
Arr::set($config, $key, $value);
382+
}
383+
384+
return $config;
385+
}, []);
386+
}
250387
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Tests\Antlers;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use Statamic\Contracts\View\Antlers\Parser;
8+
use Statamic\Facades\Antlers;
9+
use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState;
10+
use Tests\Antlers\Fixtures\MethodClasses\ClassOne;
11+
use Tests\TestCase;
12+
13+
class ParseUserContentTest extends TestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
GlobalRuntimeState::resetGlobalState();
20+
GlobalRuntimeState::$throwErrorOnAccessViolation = false;
21+
GlobalRuntimeState::$allowPhpInContent = false;
22+
GlobalRuntimeState::$allowMethodsInContent = false;
23+
}
24+
25+
#[Test]
26+
public function it_parses_templates_like_standard_parse_for_basic_content()
27+
{
28+
$this->assertSame(
29+
(string) Antlers::parse('Hello {{ name }}!', ['name' => 'Jason']),
30+
(string) Antlers::parseUserContent('Hello {{ name }}!', ['name' => 'Jason'])
31+
);
32+
}
33+
34+
#[Test]
35+
public function it_blocks_php_nodes_in_user_content_mode()
36+
{
37+
Log::shouldReceive('warning')
38+
->once()
39+
->with('PHP Node evaluated in user content: {{? echo Str::upper(\'hello\') ?}}', \Mockery::type('array'));
40+
41+
$result = (string) Antlers::parseUserContent('Text: {{? echo Str::upper(\'hello\') ?}}');
42+
43+
$this->assertSame('Text: ', $result);
44+
}
45+
46+
#[Test]
47+
public function it_blocks_method_calls_in_user_content_mode()
48+
{
49+
Log::shouldReceive('warning')
50+
->once()
51+
->with('Method call evaluated in user content.', \Mockery::type('array'));
52+
53+
$result = (string) Antlers::parseUserContent('{{ object:method("hello") }}', [
54+
'object' => new ClassOne(),
55+
]);
56+
57+
$this->assertSame('', $result);
58+
}
59+
60+
#[Test]
61+
public function it_restores_user_data_flag_after_successful_parse()
62+
{
63+
GlobalRuntimeState::$isEvaluatingUserData = false;
64+
65+
Antlers::parseUserContent('Hello {{ name }}!', ['name' => 'Jason']);
66+
67+
$this->assertFalse(GlobalRuntimeState::$isEvaluatingUserData);
68+
}
69+
70+
#[Test]
71+
public function it_restores_user_data_flag_after_parse_exceptions()
72+
{
73+
GlobalRuntimeState::$isEvaluatingUserData = false;
74+
$parser = \Mockery::mock(Parser::class);
75+
$parser->shouldReceive('parse')
76+
->once()
77+
->andThrow(new \RuntimeException('Failed to parse user content.'));
78+
79+
try {
80+
Antlers::usingParser($parser, function ($antlers) {
81+
$antlers->parseUserContent('Hello {{ name }}', ['name' => 'Jason']);
82+
});
83+
84+
$this->fail('Expected RuntimeException to be thrown.');
85+
} catch (\RuntimeException $exception) {
86+
$this->assertSame('Failed to parse user content.', $exception->getMessage());
87+
}
88+
89+
$this->assertFalse(GlobalRuntimeState::$isEvaluatingUserData);
90+
}
91+
}

tests/Sites/SitesConfigTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public function it_resolves_antlers_when_resolving_sites()
133133
]);
134134

135135
Config::set('statamic.some_addon.theme', 'sunset');
136+
Config::set('statamic.system.view_config_allowlist', ['@default', 'app.faker_locale', 'statamic.some_addon.theme']);
136137

137138
Site::setSites([
138139
'default' => [

0 commit comments

Comments
 (0)