From 6adc8d4a371291ec5e374d490c0357f1f5cfb231 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 11 Dec 2024 14:33:39 +0000 Subject: [PATCH 001/490] [5.x] Fix type error in `HandleEntrySchedule` job (#11244) --- src/Entries/MinuteEntries.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entries/MinuteEntries.php b/src/Entries/MinuteEntries.php index 5a39f73f986..13a21d52f39 100644 --- a/src/Entries/MinuteEntries.php +++ b/src/Entries/MinuteEntries.php @@ -2,13 +2,13 @@ namespace Statamic\Entries; -use Carbon\Carbon; +use Carbon\CarbonInterface; use Statamic\Facades\Collection; use Statamic\Facades\Entry; class MinuteEntries { - public function __construct(private readonly Carbon $minute) + public function __construct(private readonly CarbonInterface $minute) { } From b51b69231c2fd026ec94b8fdf79e9a22eb650ce2 Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 11 Dec 2024 14:36:52 -0600 Subject: [PATCH 002/490] [5.x] Adds "no_results" to automatic array variables (#11234) --- .../Language/Runtime/NodeProcessor.php | 1 + tests/Antlers/Runtime/TemplateTest.php | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/View/Antlers/Language/Runtime/NodeProcessor.php b/src/View/Antlers/Language/Runtime/NodeProcessor.php index b2be4c19dc6..b5dc8a5eaad 100644 --- a/src/View/Antlers/Language/Runtime/NodeProcessor.php +++ b/src/View/Antlers/Language/Runtime/NodeProcessor.php @@ -2550,6 +2550,7 @@ protected function addLoopIterationVariables($loop) $value['count'] = $index + 1; $value['index'] = $index; $value['total_results'] = $total; + $value['no_results'] = false; $value['first'] = $index === 0; $value['last'] = $index === $lastIndex; diff --git a/tests/Antlers/Runtime/TemplateTest.php b/tests/Antlers/Runtime/TemplateTest.php index b3af5515df7..bcc54ad609f 100644 --- a/tests/Antlers/Runtime/TemplateTest.php +++ b/tests/Antlers/Runtime/TemplateTest.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Facades\Log; use Illuminate\Support\MessageBag; +use Illuminate\Support\Str; use Illuminate\Support\ViewErrorBag; use Mockery; use PHPUnit\Framework\Attributes\DataProvider; @@ -22,6 +23,7 @@ use Statamic\Fields\LabeledValue; use Statamic\Fields\Value; use Statamic\Fields\Values; +use Statamic\Tags\Concerns\OutputsItems; use Statamic\Tags\Tags; use Statamic\View\Cascade; use Tests\Antlers\Fixtures\Addon\Tags\RecursiveChildren; @@ -2599,6 +2601,77 @@ public function test_it_returns_escaped_content() $input = 'Hey, look at that @{{ noun }}!'; $this->assertSame('Hey, look at that {{ noun }}!', $this->renderString($input, [])); } + + #[Test] + public function no_results_value_is_added_automatically() + { + (new class extends Tags + { + use OutputsItems; + public static $handle = 'the_tag'; + + public function index() + { + if ($this->params->get('has_value')) { + return $this->output(collect([ + 'one', + 'two', + 'three', + ])); + } + + return $this->parseNoResults(); + } + })::register(); + + $template = <<<'TEMPLATE' +{{ the_tag }} + {{ if no_results }} + No Results 1. + {{ the_tag has_value="true" }} + {{ if no_results }} + No Results 2. + {{ else }} + {{ value }} + {{ /if }} + {{ /the_tag }} + + {{ if no_results }} No Results 1.1 {{ /if }} + {{ else }} + Has Results 1. + {{ /if }} +{{ /the_tag }} +TEMPLATE; + + $this->assertSame( + 'No Results 1. one two three No Results 1.1', + Str::squish($this->renderString($template, [], true)) + ); + + $template = <<<'TEMPLATE' +{{ the_tag }} + {{ if no_results }} + No Results 1. + {{ the_tag has_value="true" as="items" }} + {{ if no_results }} + No Results 2. + {{ else }} + {{ items }}{{ value }} {{ /items }} + {{ /if }} + {{ /the_tag }} + + {{ if no_results }} No Results 1.1 {{ /if }} + {{ else }} + Has Results 1. + {{ /if }} +{{ /the_tag }} +TEMPLATE; + + $this->assertSame( + 'No Results 1. one two three No Results 1.1', + Str::squish($this->renderString($template, [], true)) + ); + } } class NonArrayableObject From 26751a9be294f01132a946af5006d3169e0f2c63 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 11 Dec 2024 15:39:15 -0500 Subject: [PATCH 003/490] [5.x] Fix subdirectory autodiscovery on Windows (#11246) --- src/Providers/ExtensionServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php index 2a32a74bc24..92432a21c56 100644 --- a/src/Providers/ExtensionServiceProvider.php +++ b/src/Providers/ExtensionServiceProvider.php @@ -336,7 +336,7 @@ protected function registerAppExtensions($folder, $requiredClass) } foreach ($this->app['files']->allFiles($path) as $file) { - $relativePathOfFolder = str_replace(app_path('/'), '', $file->getPath()); + $relativePathOfFolder = str_replace(app_path(DIRECTORY_SEPARATOR), '', $file->getPath()); $namespace = str_replace('/', '\\', $relativePathOfFolder); $class = $file->getBasename('.php'); From 7ecc2be207d4fa176c5fc5c8754cfcdfabbad308 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 11 Dec 2024 22:19:20 +0100 Subject: [PATCH 004/490] [5.x] Fix asset upload concurrency on folder upload (#11225) --- resources/js/components/assets/Uploader.vue | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/resources/js/components/assets/Uploader.vue b/resources/js/components/assets/Uploader.vue index 816c97f956c..6e0904d4e67 100644 --- a/resources/js/components/assets/Uploader.vue +++ b/resources/js/components/assets/Uploader.vue @@ -68,6 +68,15 @@ export default { }, + computed: { + + activeUploads() { + return this.uploads.filter(u => u.instance.state === 'started'); + } + + }, + + methods: { browse() { @@ -230,6 +239,9 @@ export default { }, processUploadQueue() { + // If we're already uploading, don't start another + if (this.activeUploads.length) return; + // Make sure we're not grabbing a running or failed upload const upload = this.uploads.find(u => u.instance.state === 'new' && !u.errorMessage); if (!upload) return; @@ -248,6 +260,8 @@ export default { response.status === 200 ? this.handleUploadSuccess(id, json) : this.handleUploadError(id, response.status, json); + + this.processUploadQueue(); }); }, From d62cc13484aa28ec532f814669630f8c5c4a3915 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 11 Dec 2024 16:36:25 -0500 Subject: [PATCH 005/490] changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b623051c33b..ecc966d77c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release Notes +## 5.42.1 (2024-12-11) + +### What's fixed +- Fix asset upload concurrency on folder upload [#11225](https://github.com/statamic/cms/issues/11225) by @daun +- Fix subdirectory autodiscovery on Windows [#11246](https://github.com/statamic/cms/issues/11246) by @jasonvarga +- Fix type error in `HandleEntrySchedule` job [#11244](https://github.com/statamic/cms/issues/11244) by @duncanmcclean +- Fix `no_results` cascade [#11234](https://github.com/statamic/cms/issues/11234) by @JohnathonKoster + + + ## 5.42.0 (2024-12-05) ### What's new From 684eb68224bb013bc3d988fb6c1bd75823733fd1 Mon Sep 17 00:00:00 2001 From: Alexander Jensen Date: Thu, 12 Dec 2024 16:58:02 +0100 Subject: [PATCH 006/490] [5.x] Fix collection title_format when using translations (#11248) --- src/Entries/Entry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 0de0795936b..06a18e7c0e3 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -1008,7 +1008,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()->locale(), fn () => (string) Antlers::parse($format, $this->augmented()->except('slug')->all())); + $title = $this->withLocale($this->site()->lang(), fn () => (string) Antlers::parse($format, $this->augmented()->except('slug')->all())); return trim($title); } From c02847598a921f20b6749dbe145f899626f288a5 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 12 Dec 2024 11:20:41 -0500 Subject: [PATCH 007/490] [5.x] Move bard source button into field actions (#11250) --- .../js/components/fieldtypes/bard/BardFieldtype.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/js/components/fieldtypes/bard/BardFieldtype.vue b/resources/js/components/fieldtypes/bard/BardFieldtype.vue index 98585b74f1b..2e7c1b96dcc 100644 --- a/resources/js/components/fieldtypes/bard/BardFieldtype.vue +++ b/resources/js/components/fieldtypes/bard/BardFieldtype.vue @@ -31,9 +31,6 @@ :config="config" :bard="_self" :editor="editor" /> - @@ -49,9 +46,6 @@ :config="config" :bard="_self" :editor="editor" /> - @@ -370,6 +364,12 @@ export default { visibleWhenReadOnly: true, visible: this.config.fullscreen, }, + { + title: __('Show HTML Source'), + run: () => this.showSource = !this.showSource, + visibleWhenReadOnly: true, + visible: this.allowSource, + }, ]; }, From bcd4e17e97c5013500c7d527249a505aa57e4392 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 12 Dec 2024 15:19:10 -0500 Subject: [PATCH 008/490] =?UTF-8?q?[5.x]=20Throw=20404=20on=20collection?= =?UTF-8?q?=20routes=20if=20taxonomy=20isn=E2=80=99t=20assigned=20to=20col?= =?UTF-8?q?lection=20(#10438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jason Varga --- src/Taxonomies/LocalizedTerm.php | 4 ++++ src/Taxonomies/Taxonomy.php | 4 ++++ tests/Data/Taxonomies/ViewsTest.php | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php index 86e5488372f..f40b394ae00 100644 --- a/src/Taxonomies/LocalizedTerm.php +++ b/src/Taxonomies/LocalizedTerm.php @@ -375,6 +375,10 @@ public function toResponse($request) throw new NotFoundHttpException; } + if ($this->collection() && ! $this->taxonomy()->collections()->contains($this->collection())) { + throw new NotFoundHttpException; + } + return (new DataResponse($this))->toResponse($request); } diff --git a/src/Taxonomies/Taxonomy.php b/src/Taxonomies/Taxonomy.php index b2bfd4e76e2..b7cd44e4b28 100644 --- a/src/Taxonomies/Taxonomy.php +++ b/src/Taxonomies/Taxonomy.php @@ -381,6 +381,10 @@ public function toResponse($request) throw new NotFoundHttpException; } + if ($this->collection() && ! $this->collections()->contains($this->collection())) { + throw new NotFoundHttpException; + } + return (new \Statamic\Http\Responses\DataResponse($this)) ->with([ 'terms' => $termQuery = $this->queryTerms()->where('site', $site), diff --git a/tests/Data/Taxonomies/ViewsTest.php b/tests/Data/Taxonomies/ViewsTest.php index b3175a7c7e9..812488cb9db 100644 --- a/tests/Data/Taxonomies/ViewsTest.php +++ b/tests/Data/Taxonomies/ViewsTest.php @@ -139,6 +139,18 @@ public function the_collection_specific_taxonomy_url_404s_if_the_view_doesnt_exi $this->get('/the-blog/tags/test')->assertNotFound(); } + #[Test] + public function the_collection_specific_taxonomy_url_404s_if_the_collection_is_not_configured() + { + $this->mountBlogPageToBlogCollection(); + + $this->viewShouldReturnRaw('blog.tags.index', '{{ title }} index'); + + $this->blogCollection->taxonomies([])->save(); + + $this->get('/the-blog/tags')->assertNotFound(); + } + #[Test] public function it_loads_the_collection_specific_taxonomy_url_if_the_view_exists() { @@ -157,6 +169,18 @@ public function the_collection_specific_term_url_404s_if_the_view_doesnt_exist() $this->get('/the-blog/tags/test')->assertNotFound(); } + #[Test] + public function the_collection_specific_term_url_404s_if_the_collection_is_not_assigned_to_the_taxonomy() + { + $this->mountBlogPageToBlogCollection(); + + $this->viewShouldReturnRaw('blog.tags.show', 'showing {{ title }}'); + + $this->blogCollection->taxonomies([])->save(); + + $this->get('/the-blog/tags/test')->assertNotFound(); + } + #[Test] public function it_loads_the_collection_specific_term_url_if_the_view_exists() { From 1003d402934541aefa9f409444933ed1c4cd0e11 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 12 Dec 2024 20:21:55 +0000 Subject: [PATCH 009/490] [5.x] Table Fieldtype: Add `max_columns` and `max_rows` options (#11224) Co-authored-by: Jason Varga --- resources/lang/en/fieldtypes.php | 2 ++ src/Fieldtypes/Table.php | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/resources/lang/en/fieldtypes.php b/resources/lang/en/fieldtypes.php index 7b0e3756653..f493e2394d8 100644 --- a/resources/lang/en/fieldtypes.php +++ b/resources/lang/en/fieldtypes.php @@ -169,6 +169,8 @@ 'slug.config.show_regenerate' => 'Show the regenerate button to re-slugify from the target field.', 'slug.title' => 'Slug', 'structures.title' => 'Structures', + 'table.config.max_columns' => 'Set a maximum number of columns.', + 'table.config.max_rows' => 'Set a maximum number of rows.', 'table.title' => 'Table', 'taggable.config.options' => 'Provide pre-defined tags that can be selected.', 'taggable.config.placeholder' => 'Type and press ↩ Enter', diff --git a/src/Fieldtypes/Table.php b/src/Fieldtypes/Table.php index bf235cc516c..d75e770fafc 100644 --- a/src/Fieldtypes/Table.php +++ b/src/Fieldtypes/Table.php @@ -18,6 +18,16 @@ protected function configFieldItems(): array 'instructions' => __('statamic::messages.fields_default_instructions'), 'type' => 'table', ], + 'max_rows' => [ + 'display' => __('Max Rows'), + 'instructions' => __('statamic::fieldtypes.table.config.max_rows'), + 'type' => 'integer', + ], + 'max_columns' => [ + 'display' => __('Max Columns'), + 'instructions' => __('statamic::fieldtypes.table.config.max_columns'), + 'type' => 'integer', + ], ]; } From f210188646acc713cb738dbce1f3f09c730110e4 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Thu, 12 Dec 2024 21:20:50 +0000 Subject: [PATCH 010/490] [5.x] Support glide urls with URL params (#11003) Co-authored-by: Jason Varga --- src/Imaging/GlideManager.php | 7 ++++ src/Imaging/ImageGenerator.php | 4 ++- tests/Imaging/ImageGeneratorTest.php | 50 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Imaging/GlideManager.php b/src/Imaging/GlideManager.php index ef36459de73..927490a8267 100644 --- a/src/Imaging/GlideManager.php +++ b/src/Imaging/GlideManager.php @@ -167,6 +167,13 @@ private function getCachePathCallable() $hashCallable = $this->getHashCallable(); return function ($path, $params) use ($hashCallable) { + $qs = Str::contains($path, '?') ? Str::after($path, '?') : null; + $path = Str::before($path, '?'); + + if ($qs) { + $path = Str::replaceLast('.', '-'.md5($qs).'.', $path); + } + $sourcePath = $this->getSourcePath($path); if ($this->sourcePathPrefix) { diff --git a/src/Imaging/ImageGenerator.php b/src/Imaging/ImageGenerator.php index bd38934577e..cb44c6dde75 100644 --- a/src/Imaging/ImageGenerator.php +++ b/src/Imaging/ImageGenerator.php @@ -119,12 +119,13 @@ private function doGenerateByUrl($url, array $params) $this->setParams($params); $parsed = $this->parseUrl($url); + $qs = $parsed['query']; $this->server->setSource($this->guzzleSourceFilesystem($parsed['base'])); $this->server->setSourcePathPrefix('/'); $this->server->setCachePathPrefix('http'); - return $this->generate($parsed['path']); + return $this->generate($parsed['path'].($qs ? '?'.$qs : '')); } /** @@ -330,6 +331,7 @@ private function parseUrl($url) return [ 'path' => Str::after($parsed['path'], '/'), 'base' => $parsed['scheme'].'://'.$parsed['host'], + 'query' => $parsed['query'] ?? null, ]; } } diff --git a/tests/Imaging/ImageGeneratorTest.php b/tests/Imaging/ImageGeneratorTest.php index 44bbaf745d3..b61446458ed 100644 --- a/tests/Imaging/ImageGeneratorTest.php +++ b/tests/Imaging/ImageGeneratorTest.php @@ -211,6 +211,56 @@ public function it_generates_an_image_by_external_url() Event::assertDispatchedTimes(GlideImageGenerated::class, 1); } + #[Test] + public function it_generates_an_image_by_external_url_with_query_string() + { + Event::fake(); + + $cacheKey = 'url::https://example.com/foo/hoff.jpg?query=david::4dbc41d8e3ba1ccd302641e509b48768'; + $this->assertNull(Glide::cacheStore()->get($cacheKey)); + + $this->assertCount(0, $this->generatedImagePaths()); + + $this->app->bind('statamic.imaging.guzzle', function () { + $file = UploadedFile::fake()->image('', 30, 60); + $contents = file_get_contents($file->getPathname()); + + $response = new Response(200, [], $contents); + + // Glide, Flysystem, or the Guzzle adapter will try to perform the requests + // at different points to check if the file exists or to get the content + // of it. Here we'll just mock the same response multiple times. + return new Client(['handler' => new MockHandler([ + $response, $response, $response, + ])]); + }); + + // Generate the image twice to make sure it's cached. + foreach (range(1, 2) as $i) { + $path = $this->makeGenerator()->generateByUrl( + 'https://example.com/foo/hoff.jpg?query=david', + $userParams = ['w' => 100, 'h' => 100] + ); + } + + $qsHash = md5('query=david'); + + // Since we can't really mock the server, we'll generate the md5 hash the same + // way it does. It will not include the fit parameter since it's not an asset. + $md5 = $this->getGlideMd5("foo/hoff-{$qsHash}.jpg", $userParams); + + // While writing this test I noticed that we don't include the domain in the + // cache path, so the same file path on two different domains will conflict. + // TODO: Fix this. + $expectedPath = "http/foo/hoff-{$qsHash}.jpg/{$md5}/hoff-{$qsHash}.jpg"; + + $this->assertEquals($expectedPath, $path); + $this->assertCount(1, $paths = $this->generatedImagePaths()); + $this->assertContains($expectedPath, $paths); + $this->assertEquals($expectedPath, Glide::cacheStore()->get($cacheKey)); + Event::assertDispatchedTimes(GlideImageGenerated::class, 1); + } + #[Test] public function the_watermark_disk_is_the_public_directory_by_default() { From abaa0c20082dd1d7268e96fa92b3c0112a50a6b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:32:26 -0500 Subject: [PATCH 011/490] [5.x] Bump nanoid from 3.3.6 to 3.3.8 (#11251) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb4fd1f3bcb..1cc793ed479 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7938,9 +7938,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", From 881e6c5c3412fdd027bb8e9f0649f61d521aca94 Mon Sep 17 00:00:00 2001 From: Arthur Perton Date: Mon, 16 Dec 2024 16:22:57 +0100 Subject: [PATCH 012/490] [5.x] Add some options to the static warm command to limit the number of requests (#11258) --- src/Console/Commands/StaticWarm.php | 54 ++++++++++++++ tests/Console/Commands/StaticWarmTest.php | 85 ++++++++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/StaticWarm.php b/src/Console/Commands/StaticWarm.php index 7bb26f59b67..03e33da0b53 100644 --- a/src/Console/Commands/StaticWarm.php +++ b/src/Console/Commands/StaticWarm.php @@ -38,6 +38,9 @@ class StaticWarm extends Command {--p|password= : HTTP authentication password} {--insecure : Skip SSL verification} {--uncached : Only warm uncached URLs} + {--max-depth= : Maximum depth of URLs to warm} + {--include= : Only warm specific URLs} + {--exclude= : Exclude specific URLs} '; protected $description = 'Warms the static cache by visiting all URLs'; @@ -179,6 +182,9 @@ private function uris(): Collection ->merge($this->customRouteUris()) ->merge($this->additionalUris()) ->unique() + ->filter(fn ($uri) => $this->shouldInclude($uri)) + ->reject(fn ($uri) => $this->shouldExclude($uri)) + ->reject(fn ($uri) => $this->exceedsMaxDepth($uri)) ->reject(function ($uri) use ($cacher) { if ($this->option('uncached') && $cacher->hasCachedPage(HttpRequest::create($uri))) { return true; @@ -192,6 +198,54 @@ private function uris(): Collection ->values(); } + private function shouldInclude($uri): bool + { + if (! $inclusions = $this->option('include')) { + return true; + } + + $inclusions = explode(',', $inclusions); + + return collect($inclusions)->contains(fn ($included) => $this->uriMatches($uri, $included)); + } + + private function shouldExclude($uri): bool + { + if (! $exclusions = $this->option('exclude')) { + return false; + } + + $exclusions = explode(',', $exclusions); + + return collect($exclusions)->contains(fn ($excluded) => $this->uriMatches($uri, $excluded)); + } + + private function uriMatches($uri, $pattern): bool + { + $uri = URL::makeRelative($uri); + + if (Str::endsWith($pattern, '*')) { + $prefix = Str::removeRight($pattern, '*'); + + if (Str::startsWith($uri, $prefix) && ! (Str::endsWith($prefix, '/') && $uri === $prefix)) { + return true; + } + } elseif (Str::removeRight($uri, '/') === Str::removeRight($pattern, '/')) { + return true; + } + + return false; + } + + private function exceedsMaxDepth($uri): bool + { + if (! $max = $this->option('max-depth')) { + return false; + } + + return count(explode('/', trim(URL::makeRelative($uri), '/'))) > $max; + } + private function shouldVerifySsl(): bool { if ($this->option('insecure')) { diff --git a/tests/Console/Commands/StaticWarmTest.php b/tests/Console/Commands/StaticWarmTest.php index daa5eb03d32..bcb7f5fdd87 100644 --- a/tests/Console/Commands/StaticWarmTest.php +++ b/tests/Console/Commands/StaticWarmTest.php @@ -44,7 +44,7 @@ public function it_warms_the_static_cache() } #[Test] - public function it_only_visits_uncached_urls_when_the_eco_option_is_used() + public function it_only_visits_uncached_urls_when_the_uncached_option_is_used() { $mock = Mockery::mock(Cacher::class); $mock->shouldReceive('hasCachedPage')->times(2)->andReturn(true, false); @@ -58,6 +58,89 @@ public function it_only_visits_uncached_urls_when_the_eco_option_is_used() ->assertExitCode(0); } + #[Test] + public function it_only_visits_included_urls() + { + config(['statamic.static_caching.strategy' => 'half']); + + $this->createPage('blog'); + $this->createPage('news'); + + Collection::make('blog') + ->routes('/blog/{slug}') + ->template('default') + ->save(); + + Collection::make('news') + ->routes('/news/{slug}') + ->template('default') + ->save(); + + EntryFactory::slug('post-1')->collection('blog')->id('blog-post-1')->create(); + EntryFactory::slug('post-2')->collection('blog')->id('blog-post-2')->create(); + EntryFactory::slug('article-1')->collection('news')->id('news-article-1')->create(); + EntryFactory::slug('article-2')->collection('news')->id('news-article-2')->create(); + EntryFactory::slug('article-3')->collection('news')->id('news-article-3')->create(); + + $this->artisan('statamic:static:warm', ['--include' => '/blog/post-1,/news/*']) + ->expectsOutput('Visiting 4 URLs...') + ->assertExitCode(0); + } + + #[Test] + public function it_doesnt_visit_excluded_urls() + { + config(['statamic.static_caching.strategy' => 'half']); + + $this->createPage('blog'); + $this->createPage('news'); + + Collection::make('blog') + ->routes('/blog/{slug}') + ->template('default') + ->save(); + + Collection::make('news') + ->routes('/news/{slug}') + ->template('default') + ->save(); + + EntryFactory::slug('post-1')->collection('blog')->id('blog-post-1')->create(); + EntryFactory::slug('post-2')->collection('blog')->id('blog-post-2')->create(); + EntryFactory::slug('article-1')->collection('news')->id('news-article-1')->create(); + EntryFactory::slug('article-2')->collection('news')->id('news-article-2')->create(); + EntryFactory::slug('article-3')->collection('news')->id('news-article-3')->create(); + + $this->artisan('statamic:static:warm', ['--exclude' => '/about,/contact,/blog/*,/news/article-2']) + ->expectsOutput('Visiting 4 URLs...') + ->assertExitCode(0); + } + + #[Test] + public function it_respects_max_depth() + { + config(['statamic.static_caching.strategy' => 'half']); + + Collection::make('blog') + ->routes('/awesome/blog/{slug}') + ->template('default') + ->save(); + + Collection::make('news') + ->routes('/news/{slug}') + ->template('default') + ->save(); + + EntryFactory::slug('post-1')->collection('blog')->id('blog-post-1')->create(); + EntryFactory::slug('post-2')->collection('blog')->id('blog-post-2')->create(); + EntryFactory::slug('post-3')->collection('blog')->id('blog-post-3')->create(); + EntryFactory::slug('article-1')->collection('news')->id('news-article-1')->create(); + + $this->artisan('statamic:static:warm', ['--max-depth' => 2]) + ->expectsOutput('Visiting 3 URLs...') + ->assertExitCode(0); + } + #[Test] public function it_doesnt_queue_the_requests_when_connection_is_set_to_sync() { From b884d63c1377c4256613141c6ef970069dbf36d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Ko=C5=A1t=C3=AD=C5=99?= Date: Mon, 16 Dec 2024 20:55:21 +0100 Subject: [PATCH 013/490] [5.x] OAuth: option not to create or update user during authentication (#10853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Koštíř Miloslav Co-authored-by: Jason Varga --- config/oauth.php | 38 +++++++++++ resources/views/auth/unauthorized.blade.php | 6 +- src/Http/Controllers/OAuthController.php | 47 +++++++++++-- src/OAuth/Provider.php | 20 +++++- tests/OAuth/ProviderTest.php | 75 ++++++++++++++++++++- 5 files changed, 177 insertions(+), 9 deletions(-) diff --git a/config/oauth.php b/config/oauth.php index 2b5b931b1ab..d7e3fd2488e 100644 --- a/config/oauth.php +++ b/config/oauth.php @@ -15,6 +15,44 @@ 'callback' => 'oauth/{provider}/callback', ], + /* + |-------------------------------------------------------------------------- + | Create User + |-------------------------------------------------------------------------- + | + | Whether or not a user account should be created upon authentication + | with an OAuth provider. If disabled, a user account will be need + | to be explicitly created ahead of time. + | + */ + + 'create_user' => true, + + /* + |-------------------------------------------------------------------------- + | Merge User Data + |-------------------------------------------------------------------------- + | + | When authenticating with an OAuth provider, the user data returned + | such as their name will be merged with the existing user account. + | + */ + + 'merge_user_data' => true, + + /* + |-------------------------------------------------------------------------- + | Unauthorized Redirect + |-------------------------------------------------------------------------- + | + | This controls where the user is taken after authenticating with + | an OAuth provider but their account is unauthorized. This may + | happen when the create_user option has been set to false. + | + */ + + 'unauthorized_redirect' => null, + /* |-------------------------------------------------------------------------- | Remember Me diff --git a/resources/views/auth/unauthorized.blade.php b/resources/views/auth/unauthorized.blade.php index 3502faa78ba..d781cefc793 100644 --- a/resources/views/auth/unauthorized.blade.php +++ b/resources/views/auth/unauthorized.blade.php @@ -10,7 +10,11 @@
{{ __('Unauthorized') }}
- {{ __('Log out') }} + @auth + {{ __('Log out') }} + @else + {{ __('Log in') }} + @endauth
diff --git a/src/Http/Controllers/OAuthController.php b/src/Http/Controllers/OAuthController.php index 880a6ea9227..ac98813dfac 100644 --- a/src/Http/Controllers/OAuthController.php +++ b/src/Http/Controllers/OAuthController.php @@ -36,14 +36,24 @@ public function handleProviderCallback(Request $request, string $provider) return $this->redirectToProvider($request, $provider); } - $user = $oauth->findOrCreateUser($providerUser); + if ($user = $oauth->findUser($providerUser)) { + if (config('statamic.oauth.merge_user_data', true)) { + $user = $oauth->mergeUser($user, $providerUser); + } + } elseif (config('statamic.oauth.create_user', true)) { + $user = $oauth->createUser($providerUser); + } + + if ($user) { + session()->put('oauth-provider', $provider); - session()->put('oauth-provider', $provider); + Auth::guard($request->session()->get('statamic.oauth.guard')) + ->login($user, config('statamic.oauth.remember_me', true)); - Auth::guard($request->session()->get('statamic.oauth.guard')) - ->login($user, config('statamic.oauth.remember_me', true)); + return redirect()->to($this->successRedirectUrl()); + } - return redirect()->to($this->successRedirectUrl()); + return redirect()->to($this->unauthorizedRedirectUrl()); } protected function successRedirectUrl() @@ -60,4 +70,31 @@ protected function successRedirectUrl() return Arr::get($query, 'redirect', $default); } + + protected function unauthorizedRedirectUrl() + { + // If a URL has been explicitly defined, use that. + if ($url = config('statamic.oauth.unauthorized_redirect')) { + return $url; + } + + // We'll check the redirect to see if they were intending on + // accessing the CP. If they were, we'll redirect them to + // the unauthorized page in the CP. Otherwise, to home. + + $default = '/'; + $previous = session('_previous.url'); + + if (! $query = Arr::get(parse_url($previous), 'query')) { + return $default; + } + + parse_str($query, $query); + + if (! $redirect = Arr::get($query, 'redirect')) { + return $default; + } + + return $redirect === '/'.config('statamic.cp.route') ? cp_route('unauthorized') : $default; + } } diff --git a/src/OAuth/Provider.php b/src/OAuth/Provider.php index b2000522286..0aeafdf8fe9 100644 --- a/src/OAuth/Provider.php +++ b/src/OAuth/Provider.php @@ -45,15 +45,31 @@ public function getUserId(string $id): ?string } public function findOrCreateUser($socialite): StatamicUser + { + if ($user = $this->findUser($socialite)) { + return config('statamic.oauth.merge_user_data', true) + ? $this->mergeUser($user, $socialite) + : $user; + } + + return $this->createUser($socialite); + } + + /** + * Find a Statamic user by a Socialite user. + * + * @param SocialiteUser $socialite + */ + public function findUser($socialite): ?StatamicUser { if ( ($user = User::findByOAuthId($this, $socialite->getId())) || ($user = User::findByEmail($socialite->getEmail())) ) { - return $this->mergeUser($user, $socialite); + return $user; } - return $this->createUser($socialite); + return null; } /** diff --git a/tests/OAuth/ProviderTest.php b/tests/OAuth/ProviderTest.php index e5d2edecd99..917aba151e6 100644 --- a/tests/OAuth/ProviderTest.php +++ b/tests/OAuth/ProviderTest.php @@ -74,6 +74,8 @@ public function it_merges_data() $user = $this->user()->save(); + $this->assertEquals(['name' => 'foo', 'extra' => 'bar'], $user->data()->all()); + $provider->mergeUser($user, $this->socialite()); $this->assertEquals(['name' => 'Foo Bar', 'extra' => 'bar'], $user->data()->all()); @@ -122,20 +124,91 @@ public function it_creates_a_user() } #[Test] - public function it_finds_an_existing_user_by_email() + public function it_finds_an_existing_user_via_find_user_method() + { + $provider = $this->provider(); + + $savedUser = $this->user()->save(); + + $this->assertCount(1, UserFacade::all()); + $this->assertEquals([$savedUser], UserFacade::all()->all()); + + $foundUser = $provider->findUser($this->socialite()); + + $this->assertCount(1, UserFacade::all()); + $this->assertEquals([$savedUser], UserFacade::all()->all()); + $this->assertEquals($savedUser, $foundUser); + } + + #[Test] + public function it_does_not_find_or_create_a_user_via_find_user_method() + { + $this->assertCount(0, UserFacade::all()); + + $provider = $this->provider(); + $foundUser = $provider->findUser($this->socialite()); + + $this->assertNull($foundUser); + + $this->assertCount(0, UserFacade::all()); + $user = UserFacade::all()->get(0); + $this->assertNull($user); + } + + #[Test] + public function it_finds_an_existing_user_via_find_or_create_user_method() + { + $provider = $this->provider(); + + $savedUser = $this->user()->save(); + + $this->assertCount(1, UserFacade::all()); + $this->assertEquals([$savedUser], UserFacade::all()->all()); + $this->assertEquals('foo', $savedUser->name); + + $foundUser = $provider->findOrCreateUser($this->socialite()); + + $this->assertCount(1, UserFacade::all()); + $this->assertEquals([$savedUser], UserFacade::all()->all()); + $this->assertEquals($savedUser, $foundUser); + $this->assertEquals('Foo Bar', $savedUser->name); + } + + #[Test] + public function it_finds_an_existing_user_via_find_or_create_user_method_but_doesnt_merge_data() { + config(['statamic.oauth.merge_user_data' => false]); + $provider = $this->provider(); $savedUser = $this->user()->save(); $this->assertCount(1, UserFacade::all()); $this->assertEquals([$savedUser], UserFacade::all()->all()); + $this->assertEquals('foo', $savedUser->name); $foundUser = $provider->findOrCreateUser($this->socialite()); $this->assertCount(1, UserFacade::all()); $this->assertEquals([$savedUser], UserFacade::all()->all()); $this->assertEquals($savedUser, $foundUser); + $this->assertEquals('foo', $savedUser->name); + } + + #[Test] + public function it_creates_a_user_via_find_or_create_user_method() + { + $this->assertCount(0, UserFacade::all()); + + $provider = $this->provider(); + $provider->findOrCreateUser($this->socialite()); + + $this->assertCount(1, UserFacade::all()); + $user = UserFacade::all()->get(0); + $this->assertNotNull($user); + $this->assertEquals('foo@bar.com', $user->email()); + $this->assertEquals('Foo Bar', $user->name()); + $this->assertEquals($user->id(), $provider->getUserId('foo-bar')); } #[Test] From c0318e9e3904f824c998a39b1048488a9656f8b7 Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 16 Dec 2024 23:47:13 +0100 Subject: [PATCH 014/490] [5.x] Fix ButtonGroup not showing active state if value are numbers (#10916) Co-authored-by: Jason Varga --- .../components/fieldtypes/ButtonGroupFieldtype.vue | 4 ++-- tests/Fieldtypes/HasSelectOptionsTests.php | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue b/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue index f991d0ece21..ff826d51fb1 100644 --- a/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue +++ b/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue @@ -7,10 +7,10 @@ ref="button" type="button" :name="name" - @click="updateSelectedOption($event.target.value)" + @click="updateSelectedOption(option.value)" :value="option.value" :disabled="isReadOnly" - :class="{'active': value === option.value}" + :class="{'active': value == option.value}" v-text="option.label || option.value" /> diff --git a/tests/Fieldtypes/HasSelectOptionsTests.php b/tests/Fieldtypes/HasSelectOptionsTests.php index 16719bf2cd3..849209c6f1c 100644 --- a/tests/Fieldtypes/HasSelectOptionsTests.php +++ b/tests/Fieldtypes/HasSelectOptionsTests.php @@ -14,26 +14,30 @@ public function it_preloads_options($options, $expected) $field = $this->field(['options' => $options]); $this->assertArrayHasKey('options', $preloaded = $field->preload()); - $this->assertEquals($expected, $preloaded['options']); + $this->assertSame($expected, $preloaded['options']); } public static function optionsProvider() { return [ 'list' => [ - ['one', 'two', 'three'], + ['one', 'two', 'three', 50, '100'], [ ['value' => 'one', 'label' => 'one'], ['value' => 'two', 'label' => 'two'], ['value' => 'three', 'label' => 'three'], + ['value' => 50, 'label' => 50], + ['value' => '100', 'label' => '100'], ], ], 'associative' => [ - ['one' => 'One', 'two' => 'Two', 'three' => 'Three'], + ['one' => 'One', 'two' => 'Two', 'three' => 'Three', 50 => '50', '100' => 100], [ ['value' => 'one', 'label' => 'One'], ['value' => 'two', 'label' => 'Two'], ['value' => 'three', 'label' => 'Three'], + ['value' => 50, 'label' => '50'], + ['value' => 100, 'label' => 100], ], ], 'multidimensional' => [ @@ -41,11 +45,15 @@ public static function optionsProvider() ['key' => 'one', 'value' => 'One'], ['key' => 'two', 'value' => 'Two'], ['key' => 'three', 'value' => 'Three'], + ['key' => 50, 'value' => 50], + ['key' => '100', 'value' => 100], ], [ ['value' => 'one', 'label' => 'One'], ['value' => 'two', 'label' => 'Two'], ['value' => 'three', 'label' => 'Three'], + ['value' => 50, 'label' => 50], + ['value' => '100', 'label' => 100], ], ], ]; From ae98dff984f39792ed2199f654be8e85fcb4ef89 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 17 Dec 2024 15:34:28 +0000 Subject: [PATCH 015/490] [5.x] Fix autoloading when addon has multiple service providers (#11128) Co-authored-by: Jason Varga Co-authored-by: Erin Dalzell --- src/Providers/AddonServiceProvider.php | 62 +++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/Providers/AddonServiceProvider.php b/src/Providers/AddonServiceProvider.php index c637764fd13..5667b256989 100644 --- a/src/Providers/AddonServiceProvider.php +++ b/src/Providers/AddonServiceProvider.php @@ -182,6 +182,10 @@ abstract class AddonServiceProvider extends ServiceProvider */ protected $translations = true; + private static array $autoloaded = []; + + private static array $bootedAddons = []; + public function boot() { Statamic::booted(function () { @@ -216,6 +220,8 @@ public function boot() ->bootFieldsets() ->bootPublishAfterInstall() ->bootAddon(); + + static::$bootedAddons[] = $this->getAddon()->id(); }); } @@ -454,6 +460,10 @@ protected function bootVite() protected function bootConfig() { + if (! $this->shouldBootRootItems()) { + return $this; + } + $filename = $this->getAddon()->slug(); $directory = $this->getAddon()->directory(); $origin = "{$directory}config/{$filename}.php"; @@ -473,6 +483,10 @@ protected function bootConfig() protected function bootTranslations() { + if (! $this->shouldBootRootItems()) { + return $this; + } + $slug = $this->getAddon()->slug(); $directory = $this->getAddon()->directory(); $origin = "{$directory}lang"; @@ -518,7 +532,7 @@ protected function bootRoutes() $web = Arr::get( array: $this->routes, key: 'web', - default: $this->app['files']->exists($path = $directory.'routes/web.php') ? $path : null + default: $this->shouldBootRootItems() && $this->app['files']->exists($path = $directory.'routes/web.php') ? $path : null ); if ($web) { @@ -528,7 +542,7 @@ protected function bootRoutes() $cp = Arr::get( array: $this->routes, key: 'cp', - default: $this->app['files']->exists($path = $directory.'routes/cp.php') ? $path : null + default: $this->shouldBootRootItems() && $this->app['files']->exists($path = $directory.'routes/cp.php') ? $path : null ); if ($cp) { @@ -538,7 +552,7 @@ protected function bootRoutes() $actions = Arr::get( array: $this->routes, key: 'actions', - default: $this->app['files']->exists($path = $directory.'routes/actions.php') ? $path : null + default: $this->shouldBootRootItems() && $this->app['files']->exists($path = $directory.'routes/actions.php') ? $path : null ); if ($actions) { @@ -620,6 +634,10 @@ protected function bootUpdateScripts() protected function bootViews() { + if (! $this->shouldBootRootItems()) { + return $this; + } + if (file_exists($this->getAddon()->directory().'resources/views')) { $this->loadViewsFrom( $this->getAddon()->directory().'resources/views', @@ -746,6 +764,10 @@ protected function bootPublishAfterInstall() protected function bootBlueprints() { + if (! $this->shouldBootRootItems()) { + return $this; + } + if (! file_exists($path = "{$this->getAddon()->directory()}resources/blueprints")) { return $this; } @@ -760,6 +782,10 @@ protected function bootBlueprints() protected function bootFieldsets() { + if (! $this->shouldBootRootItems()) { + return $this; + } + if (! file_exists($path = "{$this->getAddon()->directory()}resources/fieldsets")) { return $this; } @@ -783,7 +809,8 @@ protected function autoloadFilesFromFolder($folder, $requiredClass = null) return []; } - $path = $addon->directory().$addon->autoload().'/'.$folder; + $reflection = new \ReflectionClass(static::class); + $path = dirname($reflection->getFileName()).'/'.$folder; if (! $this->app['files']->exists($path)) { return []; @@ -797,19 +824,42 @@ protected function autoloadFilesFromFolder($folder, $requiredClass = null) } $class = $file->getBasename('.php'); - $fqcn = $this->namespace().'\\'.str_replace('/', '\\', $folder).'\\'.$class; + $fqcn = $reflection->getNamespaceName().'\\'.str_replace('/', '\\', $folder).'\\'.$class; if ((new \ReflectionClass($fqcn))->isAbstract() || (new \ReflectionClass($fqcn))->isInterface()) { continue; } if ($requiredClass && ! is_subclass_of($fqcn, $requiredClass)) { - return; + continue; + } + + if (in_array($fqcn, static::$autoloaded)) { + continue; } $autoloadable[] = $fqcn; + static::$autoloaded[] = $fqcn; } return $autoloadable; } + + private function shouldBootRootItems() + { + $addon = $this->getAddon(); + + // We'll keep track of addons that have been booted to ensure that multiple + // providers don't try to boot things twice. This could happen if there are + // multiple providers in the root autoload directory (src) of an addon. + if (in_array($addon->id(), static::$bootedAddons)) { + return false; + } + + // We only want to boot root items if the provider is in the autoloaded directory. + // i.e. It's the "root" provider. If it's in a subdirectory maybe the developer + // is organizing their providers. Things like tags etc. can be autoloaded but + // root level things like routes, views, config, blueprints, etc. will not. + return dirname((new \ReflectionClass(static::class))->getFileName()) === $addon->directory().$addon->autoload(); + } } From b70a97bd54e8008b94c176137148e151a260e605 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 17 Dec 2024 15:34:50 +0000 Subject: [PATCH 016/490] [5.x] Prevent "Set Alt" button from running Replace Asset action prematurely (#11269) --- resources/js/components/fieldtypes/assets/AssetRow.vue | 1 + resources/js/components/fieldtypes/assets/AssetTile.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/js/components/fieldtypes/assets/AssetRow.vue b/resources/js/components/fieldtypes/assets/AssetRow.vue index c23587b073a..b2538abebf5 100644 --- a/resources/js/components/fieldtypes/assets/AssetRow.vue +++ b/resources/js/components/fieldtypes/assets/AssetRow.vue @@ -34,6 +34,7 @@ + diff --git a/resources/js/components/field-actions/FieldAction.js b/resources/js/components/field-actions/FieldAction.js index 7e9d41be555..f2f2b19bb07 100644 --- a/resources/js/components/field-actions/FieldAction.js +++ b/resources/js/components/field-actions/FieldAction.js @@ -7,6 +7,7 @@ export default class FieldAction { #visibleWhenReadOnly; #icon; #quick; + #dangerous; #confirm; constructor(action, payload) { @@ -17,6 +18,7 @@ export default class FieldAction { this.#visibleWhenReadOnly = action.visibleWhenReadOnly ?? false; this.#icon = action.icon ?? 'image'; this.#quick = action.quick ?? false; + this.#dangerous = action.dangerous ?? false; this.title = action.title; } @@ -32,6 +34,10 @@ export default class FieldAction { return typeof this.#quick === 'function' ? this.#quick(this.#payload) : this.#quick; } + get dangerous() { + return typeof this.#dangerous === 'function' ? this.#dangerous(this.#payload) : this.#dangerous; + } + get icon() { return typeof this.#icon === 'function' ? this.#icon(this.#payload) : this.#icon; } diff --git a/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue b/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue index 8e04ce630a7..8f1418dd4c6 100644 --- a/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue +++ b/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue @@ -442,7 +442,18 @@ export default { return this.config.dynamic === 'id' ? __('statamic::fieldtypes.assets.dynamic_folder_pending_save') : __('statamic::fieldtypes.assets.dynamic_folder_pending_field', {field: `${this.config.dynamic}`}); - } + }, + + internalFieldActions() { + return [ + { + title: __('Remove All'), + dangerous: true, + run: this.removeAll, + visible: this.assets.length > 0, + }, + ]; + }, }, @@ -532,6 +543,13 @@ export default { this.assets.splice(index, 1); }, + /** + * Remove all assets from the field. + */ + removeAll() { + this.assets = []; + }, + /** * When the uploader component has finished uploading a file. */ diff --git a/resources/js/components/fieldtypes/relationship/RelationshipFieldtype.vue b/resources/js/components/fieldtypes/relationship/RelationshipFieldtype.vue index 19bb79e7da6..f8f71546979 100644 --- a/resources/js/components/fieldtypes/relationship/RelationshipFieldtype.vue +++ b/resources/js/components/fieldtypes/relationship/RelationshipFieldtype.vue @@ -147,7 +147,18 @@ export default { const item = _.findWhere(this.meta.data, { id }); return item ? item.title : id; }); - } + }, + + internalFieldActions() { + return [ + { + title: __('Unlink All'), + dangerous: true, + run: this.unlinkAll, + visible: this.value.length > 0, + }, + ]; + }, }, @@ -162,7 +173,15 @@ export default { linkExistingItem() { this.$refs.input.$refs.existing.click(); - } + }, + + unlinkAll() { + this.update([]); + this.updateMeta({ + ...this.meta, + data: [], + }); + }, } From 4ace9665fc598a50e144f8ab6f7b07bf8c2dde59 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 20 Feb 2025 09:36:20 -0500 Subject: [PATCH 097/490] [5.x] Add support for `$view` parameter closure with `Route::statamic()` (#11452) Co-authored-by: Jason Varga --- src/Http/Controllers/FrontendController.php | 33 ++- tests/Routing/RoutesTest.php | 230 +++++++++++++++++++- 2 files changed, 252 insertions(+), 11 deletions(-) diff --git a/src/Http/Controllers/FrontendController.php b/src/Http/Controllers/FrontendController.php index e6c22aeba3e..3da91b639ac 100644 --- a/src/Http/Controllers/FrontendController.php +++ b/src/Http/Controllers/FrontendController.php @@ -2,7 +2,10 @@ namespace Statamic\Http\Controllers; +use Closure; +use Illuminate\Contracts\View\View as IlluminateView; use Illuminate\Http\Request; +use ReflectionFunction; use Statamic\Auth\Protect\Protection; use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\Data; @@ -39,9 +42,26 @@ public function index(Request $request) public function route(Request $request, ...$args) { $params = $request->route()->parameters(); + $view = Arr::pull($params, 'view'); $data = Arr::pull($params, 'data'); - $data = array_merge($params, is_callable($data) ? $data(...$params) : $data); + + throw_if(is_callable($view) && $data, new \Exception('Parameter [$data] not supported with [$view] closure!')); + + if (is_callable($view)) { + $resolvedView = static::resolveRouteClosure($view, $params); + } + + if (isset($resolvedView) && $resolvedView instanceof IlluminateView) { + $view = $resolvedView->name(); + $data = $resolvedView->getData(); + } elseif (isset($resolvedView)) { + return $resolvedView; + } + + $data = array_merge($params, is_callable($data) + ? static::resolveRouteClosure($data, $params) + : $data); $view = app(View::class) ->template($view) @@ -73,4 +93,15 @@ private function getLoadedRouteItem($data) return $data; } } + + private static function resolveRouteClosure(Closure $closure, array $params) + { + $reflect = new ReflectionFunction($closure); + + $params = collect($reflect->getParameters()) + ->map(fn ($param) => $param->hasType() ? app($param->getType()->getName()) : $params[$param->getName()]) + ->all(); + + return $closure(...$params); + } } diff --git a/tests/Routing/RoutesTest.php b/tests/Routing/RoutesTest.php index ba2d5a137b6..e5c5fc8faed 100644 --- a/tests/Routing/RoutesTest.php +++ b/tests/Routing/RoutesTest.php @@ -3,6 +3,7 @@ namespace Tests\Routing; use Facades\Tests\Factories\EntryFactory; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; @@ -33,16 +34,56 @@ protected function resolveApplicationConfiguration($app) $app->booted(function () { Route::statamic('/basic-route-with-data', 'test', ['hello' => 'world']); - Route::statamic('/basic-route-with-data-from-closure', 'test', function () { + Route::statamic('/basic-route-with-view-closure', function () { + return view('test', ['hello' => 'world']); + }); + + Route::statamic('/basic-route-with-view-closure-and-dependency-injection', function (Request $request, FooClass $foo) { + return view('test', ['hello' => "view closure dependencies: $request->value $foo->value"]); + }); + + Route::statamic('/basic-route-with-view-closure-and-custom-return', function () { + return ['message' => 'not a view instance']; + }); + + Route::statamic('/basic-route-with-data-closure', 'test', function () { return ['hello' => 'world']; }); + Route::statamic('/basic-route-with-data-closure-and-dependency-injection', 'test', function (Request $request, FooClass $foo) { + return ['hello' => "data closure dependencies: $request->value $foo->value"]; + }); + + Route::statamic('/you-cannot-use-data-param-with-view-closure', function () { + return view('test', ['hello' => 'world']); + }, 'hello'); + Route::statamic('/basic-route-without-data', 'test'); Route::statamic('/route/with/placeholders/{foo}/{bar}/{baz}', 'test'); - Route::statamic('/route/with/placeholders/closure/{foo}/{bar}/{baz}', 'test', function ($foo, $bar, $baz) { - return ['hello' => "$foo $bar $baz"]; + Route::statamic('/route/with/placeholders/view/closure/{foo}/{bar}/{baz}', function ($foo, $bar, $baz) { + return view('test', ['hello' => "view closure placeholders: $foo $bar $baz"]); + }); + + Route::statamic('/route/with/placeholders/view/closure-dependency-injection/{baz}/{qux}', function (Request $request, FooClass $foo, BarClass $bar, $baz, $qux) { + return view('test', ['hello' => "view closure dependencies: $request->value $foo->value $bar->value $baz $qux"]); + }); + + Route::statamic('/route/with/placeholders/view/closure-dependency-order-doesnt-matter/{baz}/{qux}', function (FooClass $foo, $baz, BarClass $bar, Request $request, $qux) { + return view('test', ['hello' => "view closure dependencies: $request->value $foo->value $bar->value $baz $qux"]); + }); + + Route::statamic('/route/with/placeholders/data/closure/{foo}/{bar}/{baz}', 'test', function ($foo, $bar, $baz) { + return ['hello' => "data closure placeholders: $foo $bar $baz"]; + }); + + Route::statamic('/route/with/placeholders/data/closure-dependency-injection/{baz}/{qux}', 'test', function (Request $request, FooClass $foo, BarClass $bar, $baz, $qux) { + return ['hello' => "data closure dependencies: $request->value $foo->value $bar->value $baz $qux"]; + }); + + Route::statamic('/route/with/placeholders/data/closure-dependency-order-doesnt-matter/{baz}/{qux}', 'test', function (FooClass $foo, $baz, BarClass $bar, Request $request, $qux) { + return ['hello' => "data closure dependencies: $request->value $foo->value $bar->value $baz $qux"]; }); Route::statamic('/route-with-custom-layout', 'test', [ @@ -115,16 +156,108 @@ public function it_renders_a_view() } #[Test] - public function it_renders_a_view_with_data_from_a_closure() + public function it_renders_a_view_using_a_view_closure() { $this->viewShouldReturnRaw('layout', '{{ template_content }}'); $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); - $this->get('/basic-route-with-data-from-closure') + $this->get('/basic-route-with-view-closure') ->assertOk() ->assertSee('Hello world'); } + #[Test] + public function it_renders_a_view_using_a_view_closure_with_dependency_injection() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/basic-route-with-view-closure-and-dependency-injection?value=request_value') + ->assertOk() + ->assertSee('Hello view closure dependencies: request_value foo_class'); + } + + #[Test] + public function it_renders_a_view_using_a_view_closure_with_dependency_injection_from_container() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + app()->bind(FooClass::class, function () { + $foo = new FooClass; + $foo->value = 'foo_modified'; + + return $foo; + }); + + $this->get('/basic-route-with-view-closure-and-dependency-injection?value=request_value') + ->assertOk() + ->assertSee('Hello view closure dependencies: request_value foo_modified'); + } + + #[Test] + public function it_renders_a_view_using_a_custom_view_closure_that_does_not_return_a_view_instance() + { + $this->get('/basic-route-with-view-closure-and-custom-return') + ->assertOk() + ->assertJson([ + 'message' => 'not a view instance', + ]); + } + + #[Test] + public function it_renders_a_view_using_a_data_closure() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/basic-route-with-data-closure') + ->assertOk() + ->assertSee('Hello world'); + } + + #[Test] + public function it_renders_a_view_using_a_data_closure_with_dependency_injection() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/basic-route-with-data-closure-and-dependency-injection?value=request_value') + ->assertOk() + ->assertSee('Hello data closure dependencies: request_value foo_class'); + } + + #[Test] + public function it_renders_a_view_using_a_data_closure_with_dependency_injection_from_container() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + app()->bind(FooClass::class, function () { + $foo = new FooClass; + $foo->value = 'foo_modified'; + + return $foo; + }); + + $this->get('/basic-route-with-data-closure-and-dependency-injection?value=request_value') + ->assertOk() + ->assertSee('Hello data closure dependencies: request_value foo_modified'); + } + + #[Test] + public function it_throws_exception_if_you_try_to_pass_data_parameter_when_using_view_closure() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $response = $this + ->get('/you-cannot-use-data-param-with-view-closure') + ->assertInternalServerError(); + + $this->assertEquals('Parameter [$data] not supported with [$view] closure!', $response->exception->getMessage()); + } + #[Test] public function it_renders_a_view_without_data() { @@ -148,14 +281,83 @@ public function it_renders_a_view_with_placeholders() } #[Test] - public function it_renders_a_view_with_placeholders_and_data_from_a_closure() + public function it_renders_a_view_with_placeholders_using_a_view_closure() { $this->viewShouldReturnRaw('layout', '{{ template_content }}'); $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); - $this->get('/route/with/placeholders/closure/one/two/three') + $this->get('/route/with/placeholders/view/closure/one/two/three') ->assertOk() - ->assertSee('Hello one two three'); + ->assertSee('Hello view closure placeholders: one two three'); + } + + #[Test] + public function it_renders_a_view_with_placeholders_using_a_view_closure_with_dependency_injection() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/route/with/placeholders/view/closure-dependency-injection/one/two?value=request_value') + ->assertOk() + ->assertSee('Hello view closure dependencies: request_value foo_class bar_class one two'); + } + + #[Test] + public function it_renders_a_view_with_placeholders_using_a_view_closure_and_dependency_order_doesnt_matter() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + app()->bind(BarClass::class, function () { + $foo = new BarClass; + $foo->value = 'bar_class_modified'; + + return $foo; + }); + + $this->get('/route/with/placeholders/view/closure-dependency-order-doesnt-matter/one/two?value=request_value') + ->assertOk() + ->assertSee('Hello view closure dependencies: request_value foo_class bar_class_modified one two'); + } + + #[Test] + public function it_renders_a_view_with_placeholders_using_a_data_closure() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/route/with/placeholders/data/closure/one/two/three') + ->assertOk() + ->assertSee('Hello data closure placeholders: one two three'); + } + + #[Test] + public function it_renders_a_view_with_placeholders_using_a_data_closure_with_dependency_injection() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/route/with/placeholders/data/closure-dependency-injection/one/two?value=request_value') + ->assertOk() + ->assertSee('Hello data closure dependencies: request_value foo_class bar_class one two'); + } + + #[Test] + public function it_renders_a_view_with_placeholders_using_a_data_closure_and_dependency_order_doesnt_matter() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + app()->bind(BarClass::class, function () { + $foo = new BarClass; + $foo->value = 'bar_class_modified'; + + return $foo; + }); + + $this->get('/route/with/placeholders/data/closure-dependency-order-doesnt-matter/one/two?value=request_value') + ->assertOk() + ->assertSee('Hello data closure dependencies: request_value foo_class bar_class_modified one two'); } #[Test] @@ -174,7 +376,6 @@ public function it_renders_a_view_with_custom_layout() #[DataProvider('undefinedLayoutRouteProvider')] public function it_renders_a_view_without_a_layout($route) { - $this->withoutExceptionHandling(); $this->viewShouldReturnRaw('layout', 'The layout {{ template_content }}'); $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); @@ -222,7 +423,6 @@ public function it_loads_content_by_uri() #[Test] public function it_renders_a_view_with_custom_content_type() { - $this->withoutExceptionHandling(); $this->viewShouldReturnRaw('layout', '{{ template_content }}'); $this->viewShouldReturnRaw('test', '{"hello":"{{ hello }}"}'); @@ -354,3 +554,13 @@ public function it_uses_a_non_default_layout() ->assertSee('Custom layout'); } } + +class FooClass +{ + public $value = 'foo_class'; +} + +class BarClass +{ + public $value = 'bar_class'; +} From 6557fac3217605405bf4c13d4bf9692bc3e1e88c Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 21 Feb 2025 09:51:00 -0500 Subject: [PATCH 098/490] [5.x] Fix primitive type hint handling in Statamic routes (#11476) --- src/Http/Controllers/FrontendController.php | 5 +++- tests/Routing/RoutesTest.php | 30 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/FrontendController.php b/src/Http/Controllers/FrontendController.php index 3da91b639ac..f7204225cc7 100644 --- a/src/Http/Controllers/FrontendController.php +++ b/src/Http/Controllers/FrontendController.php @@ -99,7 +99,10 @@ private static function resolveRouteClosure(Closure $closure, array $params) $reflect = new ReflectionFunction($closure); $params = collect($reflect->getParameters()) - ->map(fn ($param) => $param->hasType() ? app($param->getType()->getName()) : $params[$param->getName()]) + ->map(fn ($param) => $param->hasType() && class_exists($class = $param->getType()->getName()) + ? app($class) + : $params[$param->getName()] + ) ->all(); return $closure(...$params); diff --git a/tests/Routing/RoutesTest.php b/tests/Routing/RoutesTest.php index e5c5fc8faed..932c3a4d39d 100644 --- a/tests/Routing/RoutesTest.php +++ b/tests/Routing/RoutesTest.php @@ -74,6 +74,10 @@ protected function resolveApplicationConfiguration($app) return view('test', ['hello' => "view closure dependencies: $request->value $foo->value $bar->value $baz $qux"]); }); + Route::statamic('/route/with/placeholders/view/closure-primitive-type-hints/{name}/{age}', function (string $name, int $age) { + return view('test', ['hello' => "view closure placeholders: $name $age"]); + }); + Route::statamic('/route/with/placeholders/data/closure/{foo}/{bar}/{baz}', 'test', function ($foo, $bar, $baz) { return ['hello' => "data closure placeholders: $foo $bar $baz"]; }); @@ -86,6 +90,10 @@ protected function resolveApplicationConfiguration($app) return ['hello' => "data closure dependencies: $request->value $foo->value $bar->value $baz $qux"]; }); + Route::statamic('/route/with/placeholders/data/closure-primitive-type-hints/{name}/{age}', 'test', function (string $name, int $age) { + return ['hello' => "data closure placeholders: $name $age"]; + }); + Route::statamic('/route-with-custom-layout', 'test', [ 'layout' => 'custom-layout', 'hello' => 'world', @@ -320,6 +328,17 @@ public function it_renders_a_view_with_placeholders_using_a_view_closure_and_dep ->assertSee('Hello view closure dependencies: request_value foo_class bar_class_modified one two'); } + #[Test] + public function it_renders_a_view_with_placeholders_using_a_view_closure_using_primitive_type_hints() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/route/with/placeholders/view/closure-primitive-type-hints/darth/42') + ->assertOk() + ->assertSee('Hello view closure placeholders: darth 42'); + } + #[Test] public function it_renders_a_view_with_placeholders_using_a_data_closure() { @@ -360,6 +379,17 @@ public function it_renders_a_view_with_placeholders_using_a_data_closure_and_dep ->assertSee('Hello data closure dependencies: request_value foo_class bar_class_modified one two'); } + #[Test] + public function it_renders_a_view_with_placeholders_using_a_data_closure_using_primitive_type_hints() + { + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('test', 'Hello {{ hello }}'); + + $this->get('/route/with/placeholders/data/closure-primitive-type-hints/darth/42') + ->assertOk() + ->assertSee('Hello data closure placeholders: darth 42'); + } + #[Test] public function it_renders_a_view_with_custom_layout() { From 905420146a894d1976f91ba099d813bd8d4efc39 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Fri, 21 Feb 2025 14:53:57 +0000 Subject: [PATCH 099/490] [5.x] Allow custom asset container contents cache store (#11481) --- src/Assets/AssetContainerContents.php | 18 ++++++++++++++---- tests/Assets/AssetFolderTest.php | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Assets/AssetContainerContents.php b/src/Assets/AssetContainerContents.php index 270fa1cf7f2..fa9b33e4278 100644 --- a/src/Assets/AssetContainerContents.php +++ b/src/Assets/AssetContainerContents.php @@ -35,7 +35,7 @@ public function all() return $this->files; } - return $this->files = Cache::remember($this->key(), $this->ttl(), function () { + return $this->files = $this->cacheStore()->remember($this->key(), $this->ttl(), function () { return collect($this->getRawFlysystemDirectoryListing()) ->keyBy('path') ->map(fn ($file) => $this->normalizeFlysystemAttributes($file)) @@ -164,7 +164,7 @@ private function getNormalizedFlysystemMetadata($path) public function cached() { - return Cache::get($this->key()); + return $this->cacheStore()->get($this->key()); } public function files() @@ -271,7 +271,7 @@ private function filesystem() public function save() { - Cache::put($this->key(), $this->all(), $this->ttl()); + $this->cacheStore()->put($this->key(), $this->all(), $this->ttl()); } public function forget($path) @@ -298,7 +298,7 @@ public function add($path) $files = $this->all()->put($path, $metadata); if (Statamic::isWorker()) { - Cache::put($this->key(), $files, $this->ttl()); + $this->cacheStore()->put($this->key(), $files, $this->ttl()); } $this->filteredFiles = null; @@ -316,4 +316,14 @@ private function ttl() { return Stache::isWatcherEnabled() ? 0 : null; } + + public function cacheStore() + { + return Cache::store($this->hasCustomStore() ? 'asset_container_contents' : null); + } + + private function hasCustomStore(): bool + { + return config()->has('cache.stores.asset_container_contents'); + } } diff --git a/tests/Assets/AssetFolderTest.php b/tests/Assets/AssetFolderTest.php index 24a868a49e7..e9e6093ba93 100644 --- a/tests/Assets/AssetFolderTest.php +++ b/tests/Assets/AssetFolderTest.php @@ -1151,6 +1151,24 @@ public function it_converts_to_an_array() ], $folder->toArray()); } + #[Test] + public function it_uses_a_custom_cache_store() + { + config([ + 'cache.stores.asset_container_contents' => [ + 'driver' => 'file', + 'path' => storage_path('statamic/asset-container-contents'), + ], + ]); + + Storage::fake('local'); + + $store = Facades\AssetContainer::make('test')->disk('local')->contents()->cacheStore(); + + // ideally we would have checked the store name, but laravel 10 doesnt give us a way to do that + $this->assertStringContainsString('asset-container-contents', $store->getStore()->getDirectory()); + } + private function containerWithDisk() { Storage::fake('local'); From afd32e8d13472e3ab668ce3b0f21035335177e8d Mon Sep 17 00:00:00 2001 From: Jack Sleight Date: Fri, 21 Feb 2025 14:56:41 +0000 Subject: [PATCH 100/490] [5.x] Fix cannot use paginate/limit error when one is null (#11478) --- src/Tags/Concerns/GetsQueryResults.php | 2 +- tests/Tags/Collection/EntriesTest.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Tags/Concerns/GetsQueryResults.php b/src/Tags/Concerns/GetsQueryResults.php index 766b8b22902..0e67046dc63 100644 --- a/src/Tags/Concerns/GetsQueryResults.php +++ b/src/Tags/Concerns/GetsQueryResults.php @@ -44,7 +44,7 @@ protected function allowLegacyStylePaginationLimiting() protected function preventIncompatiblePaginationParameters() { - if ($this->params->int('paginate') && $this->params->has('limit')) { + if ($this->params->int('paginate') && $this->params->int('limit')) { throw new \Exception('Cannot use [paginate] integer in combination with [limit] param.'); } diff --git a/tests/Tags/Collection/EntriesTest.php b/tests/Tags/Collection/EntriesTest.php index 96b6b3d9f81..603f6bae49c 100644 --- a/tests/Tags/Collection/EntriesTest.php +++ b/tests/Tags/Collection/EntriesTest.php @@ -142,6 +142,19 @@ public function it_should_throw_exception_if_trying_to_paginate_and_limit_at_sam $this->assertCount(3, $this->getEntries(['paginate' => 3, 'limit' => 4])); } + #[Test] + public function it_should_not_throw_exception_if_trying_to_paginate_or_limit_and_the_other_is_null() + { + $this->makeEntry('a')->save(); + $this->makeEntry('b')->save(); + $this->makeEntry('c')->save(); + $this->makeEntry('d')->save(); + $this->makeEntry('e')->save(); + + $this->assertCount(3, $this->getEntries(['paginate' => 3, 'limit' => null])); + $this->assertCount(3, $this->getEntries(['paginate' => null, 'limit' => 3])); + } + #[Test] public function it_should_throw_exception_if_trying_to_paginate_and_chunk_at_same_time() { From b9b0fe826dca13bab7f9b7bbca1a1531fcb5490c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Prai=C3=9F?= <6369555+ChristianPraiss@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:58:26 +0100 Subject: [PATCH 101/490] [5.x] Fix issue with localization files named like handles (#11482) --- src/Auth/CorePermissions.php | 2 ++ src/Auth/Permission.php | 2 ++ src/Fields/Blueprint.php | 2 ++ src/Fields/Field.php | 2 ++ src/Fields/Fieldtype.php | 2 ++ src/Fields/Tab.php | 2 ++ 6 files changed, 12 insertions(+) diff --git a/src/Auth/CorePermissions.php b/src/Auth/CorePermissions.php index 8bbf8c8a122..1931c25b8d3 100644 --- a/src/Auth/CorePermissions.php +++ b/src/Auth/CorePermissions.php @@ -12,6 +12,8 @@ use Statamic\Facades\Taxonomy; use Statamic\Facades\Utility; +use function Statamic\trans as __; + class CorePermissions { public function boot() diff --git a/src/Auth/Permission.php b/src/Auth/Permission.php index b06731a9f62..905ca852c4d 100644 --- a/src/Auth/Permission.php +++ b/src/Auth/Permission.php @@ -4,6 +4,8 @@ use Statamic\Support\Traits\FluentlyGetsAndSets; +use function Statamic\trans as __; + class Permission { use FluentlyGetsAndSets; diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php index 5235d27d2d9..c44ae5f9fd9 100644 --- a/src/Fields/Blueprint.php +++ b/src/Fields/Blueprint.php @@ -28,6 +28,8 @@ use Statamic\Support\Arr; use Statamic\Support\Str; +use function Statamic\trans as __; + class Blueprint implements Arrayable, ArrayAccess, Augmentable, QueryableValue { use ExistsAsFile, HasAugmentedData; diff --git a/src/Fields/Field.php b/src/Fields/Field.php index b7b9f4f17e9..ef9c1c9a36a 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -13,6 +13,8 @@ use Statamic\Support\Arr; use Statamic\Support\Str; +use function Statamic\trans as __; + class Field implements Arrayable { protected $handle; diff --git a/src/Fields/Fieldtype.php b/src/Fields/Fieldtype.php index 7fd86d79bfe..956f2aee447 100644 --- a/src/Fields/Fieldtype.php +++ b/src/Fields/Fieldtype.php @@ -12,6 +12,8 @@ use Statamic\Statamic; use Statamic\Support\Str; +use function Statamic\trans as __; + abstract class Fieldtype implements Arrayable { use HasHandle, RegistersItself { diff --git a/src/Fields/Tab.php b/src/Fields/Tab.php index ef0d095e894..bbc50f2a2bc 100644 --- a/src/Fields/Tab.php +++ b/src/Fields/Tab.php @@ -5,6 +5,8 @@ use Statamic\Support\Arr; use Statamic\Support\Str; +use function Statamic\trans as __; + class Tab { protected $handle; From 9142ed6f4ee5fb954c9a1603e91fd58843a2cfa7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 21 Feb 2025 15:01:58 +0000 Subject: [PATCH 102/490] [5.x] Entries Fieldtype: Hide tree view when using query scopes (#11484) --- .../js/components/inputs/relationship/RelationshipInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/inputs/relationship/RelationshipInput.vue b/resources/js/components/inputs/relationship/RelationshipInput.vue index b3554e0388b..3d890ace8ad 100644 --- a/resources/js/components/inputs/relationship/RelationshipInput.vue +++ b/resources/js/components/inputs/relationship/RelationshipInput.vue @@ -79,7 +79,7 @@ :search="search" :exclusions="exclusions" :type="config.type" - :tree="tree" + :tree="config.query_scopes?.length > 0 ? null : tree" @selected="selectionsUpdated" @closed="close" /> From d6ab488ae76c0f35fb3a0277765c2838206262cb Mon Sep 17 00:00:00 2001 From: Grischa Erbe <46897060+grischaerbe@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:37:29 +0100 Subject: [PATCH 103/490] [5.x] Fix handle dimensions of rotated videos (#11479) Co-authored-by: Jason Varga --- src/Assets/Attributes.php | 13 +++++++++++-- tests/Assets/AttributesTest.php | 25 ++++++++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Assets/Attributes.php b/src/Assets/Attributes.php index 010de336ccd..fba9435e2f5 100644 --- a/src/Assets/Attributes.php +++ b/src/Assets/Attributes.php @@ -88,9 +88,18 @@ private function getVideoAttributes() { $id3 = ExtractInfo::fromAsset($this->asset); + $width = Arr::get($id3, 'video.resolution_x'); + $height = Arr::get($id3, 'video.resolution_y'); + $rotate = Arr::get($id3, 'video.rotate', 0); + + // Adjust width and height if the video is rotated + if (in_array($rotate, [90, 270, -90, -270])) { + [$width, $height] = [$height, $width]; + } + return [ - 'width' => Arr::get($id3, 'video.resolution_x'), - 'height' => Arr::get($id3, 'video.resolution_y'), + 'width' => $width, + 'height' => $height, 'duration' => Arr::get($id3, 'playtime_seconds'), ]; } diff --git a/tests/Assets/AttributesTest.php b/tests/Assets/AttributesTest.php index 6cd3786556d..87fc3d02cab 100644 --- a/tests/Assets/AttributesTest.php +++ b/tests/Assets/AttributesTest.php @@ -6,6 +6,7 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Assets\Asset; use Statamic\Assets\Attributes; @@ -78,23 +79,37 @@ public function it_gets_the_attributes_of_audio_file() } #[Test] - public function it_gets_the_attributes_of_video_file() + #[DataProvider('videoProvider')] + public function it_gets_the_attributes_of_video_file($playtimeSeconds, $resolutionX, $resolutionY, $rotate, $expected) { $asset = (new Asset) ->container(AssetContainer::make('test-container')->disk('test')) ->path('path/to/asset.mp4'); ExtractInfo::shouldReceive('fromAsset')->with($asset)->andReturn([ - 'playtime_seconds' => 13, + 'playtime_seconds' => $playtimeSeconds, 'video' => [ - 'resolution_x' => 1920, - 'resolution_y' => 1080, + 'resolution_x' => $resolutionX, + 'resolution_y' => $resolutionY, + 'rotate' => $rotate, ], ]); $attributes = $this->attributes->asset($asset); - $this->assertEquals(['duration' => 13, 'width' => 1920, 'height' => 1080], $attributes->get()); + $this->assertEquals($expected, $attributes->get()); + } + + public static function videoProvider() + { + return [ + 'not rotated' => [13, 1920, 1080, null, ['duration' => 13, 'width' => 1920, 'height' => 1080]], + 'rotated 90' => [13, 1920, 1080, 90, ['duration' => 13, 'width' => 1080, 'height' => 1920]], + 'rotated -90' => [13, 1920, 1080, -90, ['duration' => 13, 'width' => 1080, 'height' => 1920]], + 'rotated 270' => [13, 1920, 1080, 270, ['duration' => 13, 'width' => 1080, 'height' => 1920]], + 'rotated -270' => [13, 1920, 1080, -270, ['duration' => 13, 'width' => 1080, 'height' => 1920]], + 'rotated 180' => [13, 1920, 1080, 180, ['duration' => 13, 'width' => 1920, 'height' => 1080]], + ]; } #[Test] From 1a50ee8a79e18598d42c0cd03d407659f999cf9a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 21 Feb 2025 17:28:43 +0000 Subject: [PATCH 104/490] [5.x] Carbon 3 (#11348) Co-authored-by: Jason Varga --- composer.json | 2 +- src/Assets/Asset.php | 2 +- src/Auth/File/User.php | 4 +- src/Auth/Passwords/TokenRepository.php | 2 +- src/Data/ExistsAsFile.php | 2 +- src/Data/TracksLastModified.php | 2 +- src/Entries/Entry.php | 2 +- src/Forms/Submission.php | 2 +- .../CP/SessionTimeoutController.php | 2 +- src/Http/Middleware/Localize.php | 32 +++++++++++-- src/Licensing/LicenseManager.php | 2 +- src/Licensing/Outpost.php | 2 +- src/Modifiers/CoreModifiers.php | 16 +++---- src/Revisions/RevisionRepository.php | 2 +- src/Stache/Stache.php | 2 +- src/Support/FileCollection.php | 2 +- src/Taxonomies/LocalizedTerm.php | 2 +- src/Tokens/FileTokenRepository.php | 2 +- tests/Assets/AssetTest.php | 2 +- tests/Feature/Assets/StoreAssetTest.php | 2 +- tests/Feature/Entries/EntryRevisionsTest.php | 4 +- tests/Feature/Fieldtypes/FilesTest.php | 2 +- .../GraphQL/Fieldtypes/DateFieldtypeTest.php | 14 +++++- tests/Modifiers/DaysAgoTest.php | 44 ++++++++++++++++++ tests/Modifiers/HoursAgoTest.php | 44 ++++++++++++++++++ tests/Modifiers/MinutesAgoTest.php | 44 ++++++++++++++++++ tests/Modifiers/MonthsAgoTest.php | 44 ++++++++++++++++++ tests/Modifiers/SecondsAgoTest.php | 42 +++++++++++++++++ tests/Modifiers/WeeksAgoTest.php | 45 +++++++++++++++++++ tests/Modifiers/YearsAgoTest.php | 42 +++++++++++++++++ 30 files changed, 376 insertions(+), 35 deletions(-) create mode 100644 tests/Modifiers/DaysAgoTest.php create mode 100644 tests/Modifiers/HoursAgoTest.php create mode 100644 tests/Modifiers/MinutesAgoTest.php create mode 100644 tests/Modifiers/MonthsAgoTest.php create mode 100644 tests/Modifiers/SecondsAgoTest.php create mode 100644 tests/Modifiers/WeeksAgoTest.php create mode 100644 tests/Modifiers/YearsAgoTest.php diff --git a/composer.json b/composer.json index fd4b62cef10..5fdafcae107 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "league/glide": "^2.3", "maennchen/zipstream-php": "^3.1", "michelf/php-smartypants": "^1.8.1", - "nesbot/carbon": "^2.62.1", + "nesbot/carbon": "^2.62.1 || ^3.0", "pixelfear/composer-dist-plugin": "^0.1.4", "rebing/graphql-laravel": "^9.7", "rhukster/dom-sanitizer": "^1.0.6", diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index c700ed7d7d1..53d921ee961 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -589,7 +589,7 @@ public function mimeType() */ public function lastModified() { - return Carbon::createFromTimestamp($this->meta('last_modified')); + return Carbon::createFromTimestamp($this->meta('last_modified'), config('app.timezone')); } /** diff --git a/src/Auth/File/User.php b/src/Auth/File/User.php index 4da8ee696e3..65c305b1775 100644 --- a/src/Auth/File/User.php +++ b/src/Auth/File/User.php @@ -116,7 +116,7 @@ public function lastModified() ? File::disk('users')->lastModified($path) : time(); - return Carbon::createFromTimestamp($timestamp); + return Carbon::createFromTimestamp($timestamp, config('app.timezone')); } /** @@ -298,7 +298,7 @@ public function lastLogin() { $last_login = $this->getMeta('last_login'); - return $last_login ? Carbon::createFromTimestamp($last_login) : $last_login; + return $last_login ? Carbon::createFromTimestamp($last_login, config('app.timezone')) : $last_login; } public function setLastLogin($carbon) diff --git a/src/Auth/Passwords/TokenRepository.php b/src/Auth/Passwords/TokenRepository.php index 9b226c8b5fc..0a717929fb0 100644 --- a/src/Auth/Passwords/TokenRepository.php +++ b/src/Auth/Passwords/TokenRepository.php @@ -70,7 +70,7 @@ public function exists(CanResetPasswordContract $user, $token) $record = $this->getResets()->get($user->email()); return $record && - ! $this->tokenExpired(Carbon::createFromTimestamp($record['created_at'])) + ! $this->tokenExpired(Carbon::createFromTimestamp($record['created_at'], config('app.timezone'))) && $this->hasher->check($token, $record['token']); } diff --git a/src/Data/ExistsAsFile.php b/src/Data/ExistsAsFile.php index 7a549641c01..bc1efb2e1d4 100644 --- a/src/Data/ExistsAsFile.php +++ b/src/Data/ExistsAsFile.php @@ -74,7 +74,7 @@ public function fileLastModified() return Carbon::now(); } - return Carbon::createFromTimestamp(File::lastModified($this->path())); + return Carbon::createFromTimestamp(File::lastModified($this->path()), config('app.timezone')); } public function fileExtension() diff --git a/src/Data/TracksLastModified.php b/src/Data/TracksLastModified.php index 76b4585c198..30b2f2d1b9c 100644 --- a/src/Data/TracksLastModified.php +++ b/src/Data/TracksLastModified.php @@ -10,7 +10,7 @@ trait TracksLastModified public function lastModified() { return $this->has('updated_at') - ? Carbon::createFromTimestamp($this->get('updated_at')) + ? Carbon::createFromTimestamp($this->get('updated_at'), config('app.timezone')) : $this->fileLastModified(); } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 06a18e7c0e3..decfbd60945 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -693,7 +693,7 @@ public function makeFromRevision($revision) ->slug($attrs['slug']); if ($this->collection()->dated() && ($date = Arr::get($attrs, 'date'))) { - $entry->date(Carbon::createFromTimestamp($date)); + $entry->date(Carbon::createFromTimestamp($date, config('app.timezone'))); } return $entry; diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index c279bef08a8..1442ee84fa5 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -101,7 +101,7 @@ public function columns() */ public function date() { - return Carbon::createFromTimestamp($this->id()); + return Carbon::createFromTimestamp($this->id(), config('app.timezone')); } /** diff --git a/src/Http/Controllers/CP/SessionTimeoutController.php b/src/Http/Controllers/CP/SessionTimeoutController.php index d333de9526f..a2b64830164 100644 --- a/src/Http/Controllers/CP/SessionTimeoutController.php +++ b/src/Http/Controllers/CP/SessionTimeoutController.php @@ -13,7 +13,7 @@ public function __invoke() // remember me would have already been served a 403 error and wouldn't have got this far. $lastActivity = session('last_activity', now()->timestamp); - return Carbon::createFromTimestamp($lastActivity) + return Carbon::createFromTimestamp($lastActivity, config('app.timezone')) ->addMinutes(config('session.lifetime')) ->diffInSeconds(); } diff --git a/src/Http/Middleware/Localize.php b/src/Http/Middleware/Localize.php index 17a3ecdca0f..745df44a615 100644 --- a/src/Http/Middleware/Localize.php +++ b/src/Http/Middleware/Localize.php @@ -5,8 +5,10 @@ use Carbon\Carbon; use Closure; use Illuminate\Support\Facades\Date; +use ReflectionClass; use Statamic\Facades\Site; use Statamic\Statamic; +use Statamic\Support\Arr; class Localize { @@ -29,10 +31,7 @@ public function handle($request, Closure $next) app()->setLocale($site->lang()); // Get original Carbon format so it can be restored later. - // There's no getter for it, so we'll use reflection. - $format = (new \ReflectionClass(Carbon::class))->getProperty('toStringFormat'); - $format->setAccessible(true); - $originalToStringFormat = $format->getValue(); + $originalToStringFormat = $this->getToStringFormat(); Date::setToStringFormat(Statamic::dateFormat()); $response = $next($request); @@ -45,4 +44,29 @@ public function handle($request, Closure $next) return $response; } + + /** + * This method is used to get the current toStringFormat for Carbon, in order for us + * to restore it later. There's no getter for it, so we need to use reflection. + * + * @throws \ReflectionException + */ + private function getToStringFormat(): ?string + { + $reflection = new ReflectionClass($date = Date::now()); + + // Carbon 2.x + if ($reflection->hasProperty('toStringFormat')) { + $format = $reflection->getProperty('toStringFormat'); + $format->setAccessible(true); + + return $format->getValue(); + } + + // Carbon 3.x + $factory = $reflection->getMethod('getFactory'); + $factory->setAccessible(true); + + return Arr::get($factory->invoke($date)->getSettings(), 'toStringFormat'); + } } diff --git a/src/Licensing/LicenseManager.php b/src/Licensing/LicenseManager.php index 94ca85e7c05..aa879bec118 100644 --- a/src/Licensing/LicenseManager.php +++ b/src/Licensing/LicenseManager.php @@ -35,7 +35,7 @@ public function requestRateLimited() public function failedRequestRetrySeconds() { return $this->requestRateLimited() - ? Carbon::createFromTimestamp($this->response('expiry'))->diffInSeconds() + ? (int) Carbon::createFromTimestamp($this->response('expiry'), config('app.timezone'))->diffInSeconds(absolute: true) : null; } diff --git a/src/Licensing/Outpost.php b/src/Licensing/Outpost.php index 32ffba1daa3..4385a2c0784 100644 --- a/src/Licensing/Outpost.php +++ b/src/Licensing/Outpost.php @@ -199,7 +199,7 @@ private function cacheAndReturnValidationResponse($e) private function cacheAndReturnRateLimitResponse($e) { - $seconds = $e->getResponse()->getHeader('Retry-After')[0]; + $seconds = (int) $e->getResponse()->getHeader('Retry-After')[0]; return $this->cacheResponse(now()->addSeconds($seconds), ['error' => 429]); } diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index dca01d2c578..2e8c30ec4cb 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -546,7 +546,7 @@ public function dashify($value) */ public function daysAgo($value, $params) { - return $this->carbon($value)->diffInDays(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInDays(Arr::get($params, 0))); } /** @@ -1055,7 +1055,7 @@ public function hexToRgb($value) */ public function hoursAgo($value, $params) { - return $this->carbon($value)->diffInHours(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInHours(Arr::get($params, 0))); } /** @@ -1617,7 +1617,7 @@ public function md5($value) */ public function minutesAgo($value, $params) { - return $this->carbon($value)->diffInMinutes(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInMinutes(Arr::get($params, 0))); } /** @@ -1652,7 +1652,7 @@ public function modifyDate($value, $params) */ public function monthsAgo($value, $params) { - return $this->carbon($value)->diffInMonths(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInMonths(Arr::get($params, 0))); } /** @@ -2234,7 +2234,7 @@ public function segment($value, $params, $context) */ public function secondsAgo($value, $params) { - return $this->carbon($value)->diffInSeconds(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInSeconds(Arr::get($params, 0))); } /** @@ -2933,7 +2933,7 @@ public function values($value) */ public function weeksAgo($value, $params) { - return $this->carbon($value)->diffInWeeks(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInWeeks(Arr::get($params, 0))); } /** @@ -3045,7 +3045,7 @@ public function wordCount($value) */ public function yearsAgo($value, $params) { - return $this->carbon($value)->diffInYears(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInYears(Arr::get($params, 0))); } /** @@ -3210,7 +3210,7 @@ private function getMathModifierNumber($params, $context) private function carbon($value) { if (! $value instanceof Carbon) { - $value = (is_numeric($value)) ? Date::createFromTimestamp($value) : Date::parse($value); + $value = (is_numeric($value)) ? Date::createFromTimestamp($value, config('app.timezone')) : Date::parse($value); } return $value; diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php index 6f8f778f779..88641f3268e 100644 --- a/src/Revisions/RevisionRepository.php +++ b/src/Revisions/RevisionRepository.php @@ -71,7 +71,7 @@ protected function makeRevisionFromFile($key, $path) ->key($key) ->action($yaml['action'] ?? false) ->id($date = $yaml['date']) - ->date(Carbon::createFromTimestamp($date)) + ->date(Carbon::createFromTimestamp($date, config('app.timezone'))) ->user($yaml['user'] ?? false) ->message($yaml['message'] ?? false) ->attributes($yaml['attributes']); diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 2c08f63036b..1eb76aec62d 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -176,7 +176,7 @@ public function buildDate() return null; } - return Carbon::createFromTimestamp($cache['date']); + return Carbon::createFromTimestamp($cache['date'], config('app.timezone')); } public function disableUpdatingIndexes() diff --git a/src/Support/FileCollection.php b/src/Support/FileCollection.php index 3ed53e8de90..4aebd51509a 100644 --- a/src/Support/FileCollection.php +++ b/src/Support/FileCollection.php @@ -212,7 +212,7 @@ public function toArray() 'size_mb' => $kb, 'size_gb' => $kb, 'is_file' => File::isImage($path), - 'last_modified' => Carbon::createFromTimestamp(File::lastModified($path)), + 'last_modified' => Carbon::createFromTimestamp(File::lastModified($path), config('app.timezone')), ]; } diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php index f40b394ae00..41d49858975 100644 --- a/src/Taxonomies/LocalizedTerm.php +++ b/src/Taxonomies/LocalizedTerm.php @@ -485,7 +485,7 @@ protected function defaultAugmentedRelations() public function lastModified() { return $this->has('updated_at') - ? Carbon::createFromTimestamp($this->get('updated_at')) + ? Carbon::createFromTimestamp($this->get('updated_at'), config('app.timezone')) : $this->term->fileLastModified(); } diff --git a/src/Tokens/FileTokenRepository.php b/src/Tokens/FileTokenRepository.php index 95d0d36357d..94a9caee9d9 100644 --- a/src/Tokens/FileTokenRepository.php +++ b/src/Tokens/FileTokenRepository.php @@ -55,7 +55,7 @@ private function makeFromPath(string $path): FileToken return $this ->make($token, $yaml['handler'], $yaml['data'] ?? []) - ->expireAt(Carbon::createFromTimestamp($yaml['expires_at'])); + ->expireAt(Carbon::createFromTimestamp($yaml['expires_at'], config('app.timezone'))); } public static function bindings(): array diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index 06fc356c622..3cbc1ef3d09 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -2017,7 +2017,7 @@ public function it_can_process_a_custom_image_format() public function it_appends_timestamp_to_uploaded_files_filename_if_it_already_exists() { Event::fake(); - Carbon::setTestNow(Carbon::createFromTimestamp(1549914700)); + Carbon::setTestNow(Carbon::createFromTimestamp(1549914700, config('app.timezone'))); $asset = $this->container->makeAsset('path/to/asset.jpg'); Facades\AssetContainer::shouldReceive('findByHandle')->with('test_container')->andReturn($this->container); Storage::disk('test')->put('path/to/asset.jpg', ''); diff --git a/tests/Feature/Assets/StoreAssetTest.php b/tests/Feature/Assets/StoreAssetTest.php index c417a6d0776..bba5ce6c3e0 100644 --- a/tests/Feature/Assets/StoreAssetTest.php +++ b/tests/Feature/Assets/StoreAssetTest.php @@ -171,7 +171,7 @@ public function it_can_upload_and_overwrite() #[Test] public function it_can_upload_and_append_timestamp() { - Carbon::setTestNow(Carbon::createFromTimestamp(1697379288)); + Carbon::setTestNow(Carbon::createFromTimestamp(1697379288, config('app.timezone'))); Storage::disk('test')->put('path/to/test.jpg', 'contents'); Storage::disk('test')->assertExists('path/to/test.jpg'); $this->assertCount(1, Storage::disk('test')->files('path/to')); diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index e50009df679..d9d032ead25 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -283,7 +283,7 @@ public function it_restores_a_published_entrys_working_copy_to_another_revision( $revision = tap((new Revision) ->key('collections/blog/en/123') - ->date(Carbon::createFromTimestamp('1553546421')) + ->date(Carbon::createFromTimestamp('1553546421', config('app.timezone'))) ->attributes([ 'published' => false, 'slug' => 'existing-slug', @@ -345,7 +345,7 @@ public function it_restores_an_unpublished_entrys_contents_to_another_revision() $revision = tap((new Revision) ->key('collections/blog/en/123') - ->date(Carbon::createFromTimestamp('1553546421')) + ->date(Carbon::createFromTimestamp('1553546421', config('app.timezone'))) ->attributes([ 'published' => true, 'slug' => 'existing-slug', diff --git a/tests/Feature/Fieldtypes/FilesTest.php b/tests/Feature/Fieldtypes/FilesTest.php index 70b13539ab2..38e9ce2fdbf 100644 --- a/tests/Feature/Fieldtypes/FilesTest.php +++ b/tests/Feature/Fieldtypes/FilesTest.php @@ -44,7 +44,7 @@ public function it_uploads_a_file($container, $isImage, $expectedPath, $expected ? UploadedFile::fake()->image('test.jpg', 50, 75) : UploadedFile::fake()->create('test.txt'); - Date::setTestNow(Date::createFromTimestamp(1671484636)); + Date::setTestNow(Date::createFromTimestamp(1671484636, config('app.timezone'))); $disk = Storage::fake('local'); diff --git a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php index 4cf18632ce9..776cfee9b2a 100644 --- a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php @@ -5,6 +5,8 @@ use Illuminate\Support\Carbon; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use ReflectionClass; +use Statamic\Support\Arr; #[Group('graphql')] class DateFieldtypeTest extends FieldtypeTestCase @@ -14,7 +16,17 @@ public function setUp(): void parent::setUp(); Carbon::macro('getToStringFormat', function () { - return static::$toStringFormat; + // Carbon 2.x + if (property_exists(static::this(), 'toStringFormat')) { + return static::$toStringFormat; + } + + // Carbon 3.x + $reflection = new ReflectionClass(self::this()); + $factory = $reflection->getMethod('getFactory'); + $factory->setAccessible(true); + + return Arr::get($factory->invoke(self::this())->getSettings(), 'toStringFormat'); }); } diff --git a/tests/Modifiers/DaysAgoTest.php b/tests/Modifiers/DaysAgoTest.php new file mode 100644 index 00000000000..06541a51c3d --- /dev/null +++ b/tests/Modifiers/DaysAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same time' => ['2025-02-20 00:00', 0], + 'less than a day ago' => ['2025-02-19 11:00', 0], + '1 day ago' => ['2025-02-19 00:00', 1], + '2 days ago' => ['2025-02-18 00:00', 2], + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one day from now' => ['2025-02-21 00:00', 1], + 'less than a day from now' => ['2025-02-20 13:00', 0], + 'more than a day from now' => ['2025-02-21 13:00', 1], + ]; + } + + public function modify($value) + { + return Modify::value($value)->daysAgo()->fetch(); + } +} diff --git a/tests/Modifiers/HoursAgoTest.php b/tests/Modifiers/HoursAgoTest.php new file mode 100644 index 00000000000..761bc16fb02 --- /dev/null +++ b/tests/Modifiers/HoursAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same time' => ['2025-02-20 13:10:00', 0], // 0.0 + 'less than a hour ago' => ['2025-02-20 13:00:00', 0], // 0.17 + '1 hour ago' => ['2025-02-20 12:10:00', 1], // 1.0 + '2 hours ago' => ['2025-02-20 11:10:00', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one hour from now' => ['2025-02-20 14:10:00', 1], // -1.0 + 'less than a hour from now' => ['2025-02-20 13:30:00', 0], // -0.33 + 'more than a hour from now' => ['2025-02-20 15:10:00', 2], // -2.0 + ]; + } + + public function modify($value) + { + return Modify::value($value)->hoursAgo()->fetch(); + } +} diff --git a/tests/Modifiers/MinutesAgoTest.php b/tests/Modifiers/MinutesAgoTest.php new file mode 100644 index 00000000000..b564eebc39e --- /dev/null +++ b/tests/Modifiers/MinutesAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same time' => ['2025-02-20 13:10:00', 0], // 0.0 + 'less than a minute ago' => ['2025-02-20 13:09:30', 0], // 0.5 + '1 minute ago' => ['2025-02-20 13:09:00', 1], // 1.0 + '2 minutes ago' => ['2025-02-20 13:08:00', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one minute from now' => ['2025-02-20 13:11:00', 1], // -1.0 + 'less than a minute from now' => ['2025-02-20 13:10:30', 0], // -0.5 + 'more than a minute from now' => ['2025-02-20 13:11:30', 1], // -1.5 + ]; + } + + public function modify($value) + { + return Modify::value($value)->minutesAgo()->fetch(); + } +} diff --git a/tests/Modifiers/MonthsAgoTest.php b/tests/Modifiers/MonthsAgoTest.php new file mode 100644 index 00000000000..338e5db0248 --- /dev/null +++ b/tests/Modifiers/MonthsAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same month' => ['2025-02-20', 0], // 0.0 + 'less than a month ago' => ['2025-02-10', 0], // 0.36 + '1 month ago' => ['2025-01-20', 1], // 1.0 + '2 months ago' => ['2024-12-20', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one month from now' => ['2025-03-20', 1], // -1.0 + 'less than a month from now' => ['2025-02-25', 0], // -0.18 + 'more than a month from now' => ['2025-04-20', 2], // -2.0 + ]; + } + + public function modify($value) + { + return Modify::value($value)->monthsAgo()->fetch(); + } +} diff --git a/tests/Modifiers/SecondsAgoTest.php b/tests/Modifiers/SecondsAgoTest.php new file mode 100644 index 00000000000..4a9ba572c46 --- /dev/null +++ b/tests/Modifiers/SecondsAgoTest.php @@ -0,0 +1,42 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same second' => ['2025-02-20 13:10:30', 0], // 0.0 + '1 second ago' => ['2025-02-20 13:10:29', 1], // 1.0 + '2 seconds ago' => ['2025-02-20 13:10:28', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one second from now' => ['2025-02-20 13:10:31', 1], // -1.0 + 'two seconds from now' => ['2025-02-20 13:10:32', 2], // -2.0 + ]; + } + + public function modify($value) + { + return Modify::value($value)->secondsAgo()->fetch(); + } +} diff --git a/tests/Modifiers/WeeksAgoTest.php b/tests/Modifiers/WeeksAgoTest.php new file mode 100644 index 00000000000..cb9c993ace8 --- /dev/null +++ b/tests/Modifiers/WeeksAgoTest.php @@ -0,0 +1,45 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same day' => ['2025-02-20', 0], // 0.0 + 'same week' => ['2025-02-19', 0], // 0.14 + 'less than a week ago' => ['2025-02-17', 0], // 0.43 + '1 week ago' => ['2025-02-13', 1], // 1.0 + '2 weeks ago' => ['2025-02-06', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one week from now' => ['2025-02-27', 1], // -1.0 + 'less than a week from now' => ['2025-02-22', 0], // -0.29 + 'more than a week from now' => ['2025-03-08', 2], // -2.29 + ]; + } + + public function modify($value) + { + return Modify::value($value)->weeksAgo()->fetch(); + } +} diff --git a/tests/Modifiers/YearsAgoTest.php b/tests/Modifiers/YearsAgoTest.php new file mode 100644 index 00000000000..24c58946370 --- /dev/null +++ b/tests/Modifiers/YearsAgoTest.php @@ -0,0 +1,42 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + '2 years' => ['2023-02-20', 2], // 2.0 + 'not quite 3 years' => ['2022-08-20', 2], // 2.5 + '3 years' => ['2022-02-20', 3], // 3.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + '1 year from now' => ['2026-02-20', 1], // -1.0 + 'less than a year from now' => ['2025-12-20', 0], // -0.83 + ]; + } + + public function modify($value) + { + return Modify::value($value)->yearsAgo()->fetch(); + } +} From b2591e1fc1616f745bd11e31b8296f64d82b1569 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 21 Feb 2025 12:34:08 -0500 Subject: [PATCH 105/490] changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32dcdb27c9..87de765fe8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Release Notes +## 5.48.0 (2025-03-21) + +### What's new +- Carbon 3 support [#11348](https://github.com/statamic/cms/issues/11348) by @duncanmcclean +- Allow custom asset container contents cache store [#11481](https://github.com/statamic/cms/issues/11481) by @ryanmitchell +- Add support for `$view` parameter closure with `Route::statamic()` [#11452](https://github.com/statamic/cms/issues/11452) by @jesseleite +- Add Unlink All action to relationship fields [#11475](https://github.com/statamic/cms/issues/11475) by @jacksleight + +### What's fixed +- Fix handle dimensions of rotated videos [#11479](https://github.com/statamic/cms/issues/11479) by @grischaerbe +- Entries Fieldtype: Hide tree view when using query scopes [#11484](https://github.com/statamic/cms/issues/11484) by @duncanmcclean +- Fix issue with localization files named like handles [#11482](https://github.com/statamic/cms/issues/11482) by @ChristianPraiss +- Fix cannot use paginate/limit error when one is null [#11478](https://github.com/statamic/cms/issues/11478) by @jacksleight +- Fix primitive type hint handling in Statamic routes [#11476](https://github.com/statamic/cms/issues/11476) by @jesseleite + + + ## 5.47.0 (2025-03-18) ### What's new From e9e52de6ef5e698d1d013b622fccc72de11371bc Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 21 Feb 2025 12:35:07 -0500 Subject: [PATCH 106/490] fix changelog dates --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87de765fe8f..881ec976f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Notes -## 5.48.0 (2025-03-21) +## 5.48.0 (2025-02-21) ### What's new - Carbon 3 support [#11348](https://github.com/statamic/cms/issues/11348) by @duncanmcclean @@ -17,7 +17,7 @@ -## 5.47.0 (2025-03-18) +## 5.47.0 (2025-02-18) ### What's new - Support Sections in form GraphQL queries [#11466](https://github.com/statamic/cms/issues/11466) by @ryanmitchell From b5d1172083e9b63a87e95f65d17d7c15df2a876a Mon Sep 17 00:00:00 2001 From: Emmanuel Beauchamps Date: Mon, 24 Feb 2025 11:03:48 +0100 Subject: [PATCH 107/490] [5.x] French translations (#11488) Good for 5.48 --- resources/lang/fr.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lang/fr.json b/resources/lang/fr.json index d1bc185a555..054d857a707 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -768,6 +768,7 @@ "Released on :date": "Publiée le :date", "Remember me": "Se souvenir de moi", "Remove": "Enlever", + "Remove All": "Tout enlever", "Remove all empty nodes": "Enlever tous les noeuds vides", "Remove Asset": "Enlever la ressource", "Remove child page|Remove :count child pages": "Enlever la page enfant|Enlever :count pages enfants", @@ -950,6 +951,7 @@ "Taxonomy saved": "Taxonomie enregistrée", "Template": "Modèle", "Templates": "Modèles", + "Term": "Terme", "Term created": "Terme créé", "Term deleted": "Terme supprimé", "Term references updated": "Références des termes mises à jour", @@ -1010,6 +1012,7 @@ "Uncheck All": "Décocher tout", "Underline": "Souligner", "Unlink": "Dissocier", + "Unlink All": "Tout dissocier", "Unlisted Addons": "Addons non répertoriés", "Unordered List": "Liste non ordonnée", "Unpin from Favorites": "Supprimer des favoris", From 0b256e3b02be2e3eba3e8e54862e4e6346967738 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 24 Feb 2025 15:55:40 +0000 Subject: [PATCH 108/490] [5.x] Only show spatie/fork prompt when pcntl extension is loaded (#11493) --- src/Console/Commands/InstallSsg.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Console/Commands/InstallSsg.php b/src/Console/Commands/InstallSsg.php index 0f8455dafa9..404e5b1b453 100644 --- a/src/Console/Commands/InstallSsg.php +++ b/src/Console/Commands/InstallSsg.php @@ -68,6 +68,7 @@ function () { if ( ! Composer::isInstalled('spatie/fork') + && extension_loaded('pcntl') && confirm('Would you like to install spatie/fork? It allows for running multiple workers at once.') ) { spin( From 9139cb0d20d896e024c7a34965dd539bff55958a Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 24 Feb 2025 14:17:11 -0500 Subject: [PATCH 109/490] [5.x] Fix carbon integer casting (#11496) --- src/API/AbstractCacher.php | 2 +- src/Git/Git.php | 2 +- src/GraphQL/ResponseCache/DefaultCache.php | 2 +- src/Http/Controllers/CP/SessionTimeoutController.php | 2 +- src/Providers/CacheServiceProvider.php | 2 +- src/StaticCaching/Cachers/AbstractCacher.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/API/AbstractCacher.php b/src/API/AbstractCacher.php index 12f964c6db4..65297750598 100644 --- a/src/API/AbstractCacher.php +++ b/src/API/AbstractCacher.php @@ -55,6 +55,6 @@ protected function normalizeKey($key) */ public function cacheExpiry() { - return Carbon::now()->addMinutes($this->config('expiry')); + return Carbon::now()->addMinutes((int) $this->config('expiry')); } } diff --git a/src/Git/Git.php b/src/Git/Git.php index a19b24d27b4..5dad7536808 100644 --- a/src/Git/Git.php +++ b/src/Git/Git.php @@ -85,7 +85,7 @@ public function commit($message = null) public function dispatchCommit($message = null) { if ($delay = config('statamic.git.dispatch_delay')) { - $delayInMinutes = now()->addMinutes($delay); + $delayInMinutes = now()->addMinutes((int) $delay); $message = null; } diff --git a/src/GraphQL/ResponseCache/DefaultCache.php b/src/GraphQL/ResponseCache/DefaultCache.php index baf313376f1..d49014bce31 100644 --- a/src/GraphQL/ResponseCache/DefaultCache.php +++ b/src/GraphQL/ResponseCache/DefaultCache.php @@ -19,7 +19,7 @@ public function put(Request $request, $response) { $key = $this->track($request); - $ttl = Carbon::now()->addMinutes(config('statamic.graphql.cache.expiry', 60)); + $ttl = Carbon::now()->addMinutes((int) config('statamic.graphql.cache.expiry', 60)); Cache::put($key, $response, $ttl); } diff --git a/src/Http/Controllers/CP/SessionTimeoutController.php b/src/Http/Controllers/CP/SessionTimeoutController.php index a2b64830164..4278b616143 100644 --- a/src/Http/Controllers/CP/SessionTimeoutController.php +++ b/src/Http/Controllers/CP/SessionTimeoutController.php @@ -14,7 +14,7 @@ public function __invoke() $lastActivity = session('last_activity', now()->timestamp); return Carbon::createFromTimestamp($lastActivity, config('app.timezone')) - ->addMinutes(config('session.lifetime')) + ->addMinutes((int) config('session.lifetime')) ->diffInSeconds(); } } diff --git a/src/Providers/CacheServiceProvider.php b/src/Providers/CacheServiceProvider.php index 6a598cf431a..064d2b88126 100644 --- a/src/Providers/CacheServiceProvider.php +++ b/src/Providers/CacheServiceProvider.php @@ -76,7 +76,7 @@ private function macroRememberWithExpiration() $keyValuePair = $callback(); $value = reset($keyValuePair); - $expiration = Carbon::now()->addMinutes(key($keyValuePair)); + $expiration = Carbon::now()->addMinutes((int) key($keyValuePair)); return Cache::remember($cacheKey, $expiration, function () use ($value) { return $value; diff --git a/src/StaticCaching/Cachers/AbstractCacher.php b/src/StaticCaching/Cachers/AbstractCacher.php index 4f81543fd72..22a7adf2163 100644 --- a/src/StaticCaching/Cachers/AbstractCacher.php +++ b/src/StaticCaching/Cachers/AbstractCacher.php @@ -66,7 +66,7 @@ public function getBaseUrl() */ public function getDefaultExpiration() { - return $this->config('expiry'); + return (int) $this->config('expiry'); } /** From 18c0d137e46d573ba5f6168add3d431daea74d88 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 25 Feb 2025 14:52:23 +0000 Subject: [PATCH 110/490] [5.x] Remove duplicate translation line from `translator` command (#11494) Co-authored-by: Jason Varga --- resources/lang/az.json | 2 +- resources/lang/cs.json | 2 +- resources/lang/da.json | 2 +- resources/lang/de.json | 2 +- resources/lang/de_CH.json | 2 +- resources/lang/es.json | 2 +- resources/lang/fa.json | 2 +- resources/lang/fr.json | 2 +- resources/lang/hu.json | 2 +- translator | 1 - 10 files changed, 9 insertions(+), 10 deletions(-) diff --git a/resources/lang/az.json b/resources/lang/az.json index a0aaad55f67..da103825c11 100644 --- a/resources/lang/az.json +++ b/resources/lang/az.json @@ -528,7 +528,7 @@ "Icon": "İkon", "ID": "ID", "ID regenerated and Stache cleared": "ID yenidən yaradıldı və Stache təmizləndi", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Əgər \":actionText\" düyməsini tıklamaqda çətinlik çəkirsinizsə, aşağıdakı URL-ni kopyalayıb veb brauzerinizə yapışdırın:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Əgər \":actionText\" düyməsini tıklamaqda çətinlik çəkirsinizsə, aşağıdakı URL-ni kopyalayıb veb brauzerinizə yapışdırın:", "Image": "Şəkil", "Image Cache": "Şəkil Keşi", "Image cache cleared.": "Şəkil keşi təmizləndi.", diff --git a/resources/lang/cs.json b/resources/lang/cs.json index 6727896c682..655855886ec 100644 --- a/resources/lang/cs.json +++ b/resources/lang/cs.json @@ -528,7 +528,7 @@ "Icon": "Ikona", "ID": "ID", "ID regenerated and Stache cleared": "ID obnoveno a Stache vyprázdněno", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Pokud máte problém s kliknutím na tlačítko \":actionText\", zkopírujte a vložte URL níže do webového prohlížeče:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Pokud máte problém s kliknutím na tlačítko \":actionText\", zkopírujte a vložte URL níže do webového prohlížeče:", "Image": "Obrázek", "Image Cache": "Cache obrázků", "Image cache cleared.": "Cache obrázků smazána.", diff --git a/resources/lang/da.json b/resources/lang/da.json index 634f0b7087d..16d81a021bd 100644 --- a/resources/lang/da.json +++ b/resources/lang/da.json @@ -528,7 +528,7 @@ "Icon": "Ikon", "ID": "ID", "ID regenerated and Stache cleared": "ID regenereret og Stache ryddet", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Hvis du har problemer med at klikke på knappen \" :actionText \", skal du kopiere og indsætte nedenstående URL i din webbrowser:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Hvis du har problemer med at klikke på knappen \" :actionText \", skal du kopiere og indsætte nedenstående URL i din webbrowser:", "Image": "Billede", "Image Cache": "Billedcache", "Image cache cleared.": "Billedcache ryddet.", diff --git a/resources/lang/de.json b/resources/lang/de.json index 5210ebd3574..8266c8aded8 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -528,7 +528,7 @@ "Icon": "Icon", "ID": "ID", "ID regenerated and Stache cleared": "ID regeneriert und Stache gelöscht", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Wenn du beim Klicken auf den Button „:actionText“ Probleme hast, kopiere die folgende URL und füge sie in deinem Browser ein:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Wenn du beim Klicken auf den Button „:actionText“ Probleme hast, kopiere die folgende URL und füge sie in deinem Browser ein:", "Image": "Bild", "Image Cache": "Bildercache", "Image cache cleared.": "Der Bildercache wurde gelöscht.", diff --git a/resources/lang/de_CH.json b/resources/lang/de_CH.json index c471f625b58..6da9859473a 100644 --- a/resources/lang/de_CH.json +++ b/resources/lang/de_CH.json @@ -528,7 +528,7 @@ "Icon": "Icon", "ID": "ID", "ID regenerated and Stache cleared": "ID regeneriert und Stache gelöscht", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Wenn du beim Klicken auf den Button «:actionText» Probleme hast, kopiere die folgende URL und füge sie in deinem Browser ein:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Wenn du beim Klicken auf den Button «:actionText» Probleme hast, kopiere die folgende URL und füge sie in deinem Browser ein:", "Image": "Bild", "Image Cache": "Bildercache", "Image cache cleared.": "Der Bildercache wurde gelöscht.", diff --git a/resources/lang/es.json b/resources/lang/es.json index 58698bd2b3e..63ba945dfdb 100644 --- a/resources/lang/es.json +++ b/resources/lang/es.json @@ -528,7 +528,7 @@ "Icon": "Icono", "ID": "IDENTIFICACIÓN", "ID regenerated and Stache cleared": "ID regenerada y bigote limpiado", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si tienes problemas para hacer clic en el botón de \":actionText\", copia y pega el siguiente URL en tu navegador:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si tienes problemas para hacer clic en el botón de \":actionText\", copia y pega el siguiente URL en tu navegador:", "Image": "Imagen", "Image Cache": "Caché de imágenes", "Image cache cleared.": "Caché de imágenes borrado.", diff --git a/resources/lang/fa.json b/resources/lang/fa.json index d597b207dd8..c8734555030 100644 --- a/resources/lang/fa.json +++ b/resources/lang/fa.json @@ -528,7 +528,7 @@ "Icon": "آیکون", "ID": "شناسه", "ID regenerated and Stache cleared": "شناسه با موفقیت از نو ساخته و کش خالی شد", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "اگر مشکلی در خصوص کلیلک روی دکمه‌ی \":actionText\" وجود دارد، می‌توانید آدرس زیر را کپی و در آدرس بار مرورگر پیست کنید:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "اگر مشکلی در خصوص کلیلک روی دکمه‌ی \":actionText\" وجود دارد، می‌توانید آدرس زیر را کپی و در آدرس بار مرورگر پیست کنید:", "Image": "تصویر", "Image Cache": "کش تصویر", "Image cache cleared.": "کش تصویر پاک شد", diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 054d857a707..fc8e148f360 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -528,7 +528,7 @@ "Icon": "Icône", "ID": "ID", "ID regenerated and Stache cleared": "ID regénéré et Stache effacé", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si vous ne parvenez pas à cliquer sur le bouton \":actionText\", copiez et collez l'URL ci-dessous dans votre navigateur Web :", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si vous ne parvenez pas à cliquer sur le bouton \":actionText\", copiez et collez l'URL ci-dessous dans votre navigateur Web :", "Image": "Image", "Image Cache": "Cache des images", "Image cache cleared.": "Cache des images effacé.", diff --git a/resources/lang/hu.json b/resources/lang/hu.json index 9595fa7431f..a023f82a11e 100644 --- a/resources/lang/hu.json +++ b/resources/lang/hu.json @@ -528,7 +528,7 @@ "Icon": "Ikon", "ID": "ID", "ID regenerated and Stache cleared": "ID újragenerálva, Stache törölve", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Ha nem tudja megkattintani a(z) \":actionText\" gombot, másolja ki és illessze be az alábbi URL-t a böngészőjébe:", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Ha nem tudja megkattintani a(z) \":actionText\" gombot, másolja ki és illessze be az alábbi URL-t a böngészőjébe:", "Image": "Kép", "Image Cache": "Képgyorsítótár", "Image cache cleared.": "A képgyorsítótár törölve.", diff --git a/translator b/translator index f53294f5bd0..eca1e5f6443 100644 --- a/translator +++ b/translator @@ -46,7 +46,6 @@ $additionalStrings = [ 'Whoops!', 'Regards', "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:", - "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:", // laravel <8.48.0 'All rights reserved.', 'The given data was invalid.', 'Protected Page', From 7f8ec820e072274bf5d007eb44737f7e9ed85021 Mon Sep 17 00:00:00 2001 From: Daryl <32465543+dmxmo@users.noreply.github.com> Date: Tue, 25 Feb 2025 06:52:43 -0800 Subject: [PATCH 111/490] [5.x] Fix: Include port in CSP for Live Preview (#11498) Co-authored-by: Duncan McClean Co-authored-by: Jason Varga --- src/Tokens/Handlers/LivePreview.php | 4 +++- .../Entries/AddsHeadersToLivePreviewTest.php | 13 +++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Tokens/Handlers/LivePreview.php b/src/Tokens/Handlers/LivePreview.php index 39e4fbc7d7a..9f4e8e3de2c 100644 --- a/src/Tokens/Handlers/LivePreview.php +++ b/src/Tokens/Handlers/LivePreview.php @@ -39,6 +39,8 @@ private function getSchemeAndHost(Site $site): string { $parts = parse_url($site->absoluteUrl()); - return $parts['scheme'].'://'.$parts['host']; + $port = isset($parts['port']) ? ':'.$parts['port'] : ''; + + return $parts['scheme'].'://'.$parts['host'].$port; } } diff --git a/tests/Feature/Entries/AddsHeadersToLivePreviewTest.php b/tests/Feature/Entries/AddsHeadersToLivePreviewTest.php index c25ee610d25..928044a50ee 100644 --- a/tests/Feature/Entries/AddsHeadersToLivePreviewTest.php +++ b/tests/Feature/Entries/AddsHeadersToLivePreviewTest.php @@ -58,17 +58,22 @@ public function it_doesnt_set_header_when_single_site() public function it_sets_header_when_multisite() { config()->set('statamic.system.multisite', true); + $this->setSites([ - 'en' => ['url' => 'http://localhost/', 'locale' => 'en'], - 'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'], - 'third' => ['url' => 'http://third/', 'locale' => 'en'], + 'one' => ['url' => 'http://withport.com:8080/', 'locale' => 'en'], + 'two' => ['url' => 'http://withport.com:8080/fr/', 'locale' => 'fr'], + 'three' => ['url' => 'http://withoutport.com/', 'locale' => 'en'], + 'four' => ['url' => 'http://withoutport.com/fr/', 'locale' => 'fr'], + 'five' => ['url' => 'http://third.com/', 'locale' => 'en'], + 'six' => ['url' => 'http://third.com/fr/', 'locale' => 'fr'], ]); + $substitute = EntryFactory::collection('test')->id('2')->slug('charlie')->data(['title' => 'Substituted title', 'foo' => 'Substituted foo'])->make(); LivePreview::tokenize('test-token', $substitute); $this->get('/test?token=test-token') ->assertHeader('X-Statamic-Live-Preview', true) - ->assertHeader('Content-Security-Policy', 'frame-ancestors http://localhost http://third'); + ->assertHeader('Content-Security-Policy', 'frame-ancestors http://withport.com:8080 http://withoutport.com http://third.com'); } } From 600ecc537a80816453a9993c42bffb7ba42ee969 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 25 Feb 2025 11:38:05 -0500 Subject: [PATCH 112/490] [5.x] Fix session expiry component (#11501) --- src/Http/Controllers/CP/SessionTimeoutController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/SessionTimeoutController.php b/src/Http/Controllers/CP/SessionTimeoutController.php index 4278b616143..f4091039e63 100644 --- a/src/Http/Controllers/CP/SessionTimeoutController.php +++ b/src/Http/Controllers/CP/SessionTimeoutController.php @@ -13,8 +13,8 @@ public function __invoke() // remember me would have already been served a 403 error and wouldn't have got this far. $lastActivity = session('last_activity', now()->timestamp); - return Carbon::createFromTimestamp($lastActivity, config('app.timezone')) + return abs((int) Carbon::createFromTimestamp($lastActivity, config('app.timezone')) ->addMinutes((int) config('session.lifetime')) - ->diffInSeconds(); + ->diffInSeconds()); } } From 6af62e586fabde581de1d350978a2b2a3d88dcf2 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 25 Feb 2025 11:41:41 -0500 Subject: [PATCH 113/490] changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 881ec976f34..41edc632b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Release Notes +## 5.48.1 (2025-02-25) + +### What's fixed +- Fix session expiry component [#11501](https://github.com/statamic/cms/issues/11501) by @jasonvarga +- Include port in CSP for Live Preview [#11498](https://github.com/statamic/cms/issues/11498) by @dmxmo +- Remove duplicate translation line from `translator` command [#11494](https://github.com/statamic/cms/issues/11494) by @duncanmcclean +- Fix carbon integer casting [#11496](https://github.com/statamic/cms/issues/11496) by @jasonvarga +- Only show spatie/fork prompt when pcntl extension is loaded [#11493](https://github.com/statamic/cms/issues/11493) by @duncanmcclean +- French translations [#11488](https://github.com/statamic/cms/issues/11488) by @ebeauchamps + + + ## 5.48.0 (2025-02-21) ### What's new From b35e734eeb2472028b66b72f5fba40eac3dc0c3b Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Tue, 25 Feb 2025 20:29:27 +0100 Subject: [PATCH 114/490] [5.x] Asset Container returns relative url for same site (#11372) Co-authored-by: Jason Varga --- src/Assets/AssetContainer.php | 5 ++++- tests/Assets/AssetContainerTest.php | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Assets/AssetContainer.php b/src/Assets/AssetContainer.php index 29765fce499..86263865e7e 100644 --- a/src/Assets/AssetContainer.php +++ b/src/Assets/AssetContainer.php @@ -27,6 +27,7 @@ use Statamic\Facades\Stache; use Statamic\Facades\URL; use Statamic\Support\Arr; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; class AssetContainer implements Arrayable, ArrayAccess, AssetContainerContract, Augmentable @@ -138,7 +139,9 @@ public function url() return null; } - $url = rtrim($this->disk()->url('/'), '/'); + $url = (string) Str::of($this->disk()->url('/')) + ->rtrim('/') + ->after(config('app.url')); return ($url === '') ? '/' : $url; } diff --git a/tests/Assets/AssetContainerTest.php b/tests/Assets/AssetContainerTest.php index 507c7358792..9bc8f92ffed 100644 --- a/tests/Assets/AssetContainerTest.php +++ b/tests/Assets/AssetContainerTest.php @@ -137,6 +137,21 @@ public function it_gets_the_url_from_the_disk_config_when_its_relative() $this->assertEquals('http://localhost/container', $container->absoluteUrl()); } + #[Test] + public function it_gets_the_url_from_the_disk_config_when_its_app_url() + { + config(['filesystems.disks.test' => [ + 'driver' => 'local', + 'root' => __DIR__.'/__fixtures__/container', + 'url' => 'http://localhost/container', + ]]); + + $container = (new AssetContainer)->disk('test'); + + $this->assertEquals('/container', $container->url()); + $this->assertEquals('http://localhost/container', $container->absoluteUrl()); + } + #[Test] public function its_private_if_the_disk_has_no_url() { From ec8e5c72785be78fd838dda81dc48ff8b6e6cad7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 25 Feb 2025 19:42:42 +0000 Subject: [PATCH 115/490] [5.x] Laravel 12 (#11433) Co-authored-by: Jason Varga --- .github/workflows/tests.yml | 4 +- composer.json | 12 +-- .../LaravelTwelveTokenRepository.php | 98 +++++++++++++++++++ src/Auth/Passwords/PasswordBrokerManager.php | 15 ++- tests/Auth/Protect/PasswordEntryTest.php | 12 +-- 5 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 src/Auth/Passwords/LaravelTwelveTokenRepository.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f222fa9f1f..f60b3bf6ff2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: php: [8.1, 8.2, 8.3, 8.4] - laravel: [10.*, 11.*] + laravel: [10.*, 11.*, 12.*] stability: [prefer-lowest, prefer-stable] os: [ubuntu-latest] include: @@ -32,6 +32,8 @@ jobs: exclude: - php: 8.1 laravel: 11.* + - php: 8.1 + laravel: 12.* - php: 8.4 laravel: 10.* diff --git a/composer.json b/composer.json index 5fdafcae107..a2934dd6a1b 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "composer/semver": "^3.4", "guzzlehttp/guzzle": "^6.3 || ^7.0", "james-heinrich/getid3": "^1.9.21", - "laravel/framework": "^10.40 || ^11.34", + "laravel/framework": "^10.40 || ^11.34 || ^12.0", "laravel/prompts": "^0.1.16 || ^0.2.0 || ^0.3.0", "league/commonmark": "^2.2", "league/csv": "^9.0", @@ -23,12 +23,12 @@ "michelf/php-smartypants": "^1.8.1", "nesbot/carbon": "^2.62.1 || ^3.0", "pixelfear/composer-dist-plugin": "^0.1.4", - "rebing/graphql-laravel": "^9.7", + "rebing/graphql-laravel": "^9.8", "rhukster/dom-sanitizer": "^1.0.6", "spatie/blink": "^1.3", - "spatie/ignition": "^1.15", + "spatie/ignition": "^1.15.1", "statamic/stringy": "^3.1.2", - "stillat/blade-parser": "^1.10.1", + "stillat/blade-parser": "^1.10.1 || ^2.0", "symfony/lock": "^6.4", "symfony/var-exporter": "^6.0", "symfony/yaml": "^6.0 || ^7.0", @@ -42,8 +42,8 @@ "google/cloud-translate": "^1.6", "laravel/pint": "1.16.0", "mockery/mockery": "^1.6.10", - "orchestra/testbench": "^8.14 || ^9.2", - "phpunit/phpunit": "^10.5.35", + "orchestra/testbench": "^8.14 || ^9.2 || ^10.0", + "phpunit/phpunit": "^10.5.35 || ^11.5.3", "spatie/laravel-ray": "^1.37" }, "config": { diff --git a/src/Auth/Passwords/LaravelTwelveTokenRepository.php b/src/Auth/Passwords/LaravelTwelveTokenRepository.php new file mode 100644 index 00000000000..17b2891cc2a --- /dev/null +++ b/src/Auth/Passwords/LaravelTwelveTokenRepository.php @@ -0,0 +1,98 @@ +path = storage_path("statamic/password_resets/$table.yaml"); + } + + public function create(CanResetPasswordContract $user) + { + $email = $user->getEmailForPasswordReset(); + + $token = $this->createNewToken(); + + $this->insert($this->getPayload($email, $token)); + + return $token; + } + + protected function insert($payload) + { + $resets = $this->getResets(); + + $resets[$payload['email']] = [ + 'token' => $payload['token'], + 'created_at' => $payload['created_at']->timestamp, + ]; + + $this->putResets($resets); + } + + public function delete(CanResetPasswordContract $user) + { + $this->putResets( + $this->getResets()->forget($user->email()) + ); + } + + public function deleteExpired() + { + $this->putResets($this->getResets()->reject(function ($item, $email) { + return $this->tokenExpired($item['created_at']); + })); + } + + public function exists(CanResetPasswordContract $user, $token) + { + $record = $this->getResets()->get($user->email()); + + return $record && + ! $this->tokenExpired(Carbon::createFromTimestamp($record['created_at'], config('app.timezone'))) + && $this->hasher->check($token, $record['token']); + } + + public function recentlyCreatedToken(CanResetPasswordContract $user) + { + $record = $this->getResets()->get($user->email()); + + return $record && parent::tokenRecentlyCreated($record['created_at']); + } + + protected function getResets() + { + if (! $this->files->exists($this->path)) { + return collect(); + } + + return collect(YAML::parse($this->files->get($this->path))); + } + + protected function putResets($resets) + { + if (! $this->files->isDirectory($dir = dirname($this->path))) { + $this->files->makeDirectory($dir); + } + + $this->files->put($this->path, YAML::dump($resets->all())); + } +} diff --git a/src/Auth/Passwords/PasswordBrokerManager.php b/src/Auth/Passwords/PasswordBrokerManager.php index 3e70c7cd147..0267a1e2acd 100644 --- a/src/Auth/Passwords/PasswordBrokerManager.php +++ b/src/Auth/Passwords/PasswordBrokerManager.php @@ -15,12 +15,23 @@ protected function createTokenRepository(array $config) $key = base64_decode(substr($key, 7)); } - return new TokenRepository( + if (version_compare(app()->version(), '12', '<')) { + return new TokenRepository( + $this->app['files'], + $this->app['hash'], + $config['table'], + $key, + $config['expire'], + $config['throttle'] ?? 0 + ); + } + + return new LaravelTwelveTokenRepository( $this->app['files'], $this->app['hash'], $config['table'], $key, - $config['expire'], + ($config['expire'] ?? 60) * 60, $config['throttle'] ?? 0 ); } diff --git a/tests/Auth/Protect/PasswordEntryTest.php b/tests/Auth/Protect/PasswordEntryTest.php index cd8a89e1dd3..371912b833e 100644 --- a/tests/Auth/Protect/PasswordEntryTest.php +++ b/tests/Auth/Protect/PasswordEntryTest.php @@ -110,16 +110,16 @@ public static function localPasswordProvider() { return [ 'string' => [ - 'value' => 'the-local-password', - 'submitted' => 'the-local-password', + 'the-local-password', + 'the-local-password', ], 'array with single value' => [ - 'value' => ['the-local-password'], - 'submitted' => 'the-local-password', + ['the-local-password'], + 'the-local-password', ], 'array with multiple values' => [ - 'value' => ['first-local-password', 'second-local-password'], - 'submitted' => 'second-local-password', + ['first-local-password', 'second-local-password'], + 'second-local-password', ], ]; } From da2431819316bcb72d2bf763e0fe83d8546746d9 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 25 Feb 2025 15:14:29 -0500 Subject: [PATCH 116/490] changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41edc632b56..5d11289b912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release Notes +## 5.49.0 (2025-02-25) + +### What's new +- Laravel 12 support [#11433](https://github.com/statamic/cms/issues/11433) by @duncanmcclean + +### What's fixed +- Asset Container returns relative url for same site [#11372](https://github.com/statamic/cms/issues/11372) by @marcorieser + + + ## 5.48.1 (2025-02-25) ### What's fixed From dd8f37b648617468243d8de0ef30756fe77aa717 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 26 Feb 2025 11:10:25 -0500 Subject: [PATCH 117/490] [5.x] Alternate Laravel 12 token repository fix (#11505) --- .../LaravelTwelveTokenRepository.php | 98 ------------------- src/Auth/Passwords/PasswordBrokerManager.php | 15 +-- src/Auth/Passwords/TokenRepository.php | 3 - 3 files changed, 2 insertions(+), 114 deletions(-) delete mode 100644 src/Auth/Passwords/LaravelTwelveTokenRepository.php diff --git a/src/Auth/Passwords/LaravelTwelveTokenRepository.php b/src/Auth/Passwords/LaravelTwelveTokenRepository.php deleted file mode 100644 index 17b2891cc2a..00000000000 --- a/src/Auth/Passwords/LaravelTwelveTokenRepository.php +++ /dev/null @@ -1,98 +0,0 @@ -path = storage_path("statamic/password_resets/$table.yaml"); - } - - public function create(CanResetPasswordContract $user) - { - $email = $user->getEmailForPasswordReset(); - - $token = $this->createNewToken(); - - $this->insert($this->getPayload($email, $token)); - - return $token; - } - - protected function insert($payload) - { - $resets = $this->getResets(); - - $resets[$payload['email']] = [ - 'token' => $payload['token'], - 'created_at' => $payload['created_at']->timestamp, - ]; - - $this->putResets($resets); - } - - public function delete(CanResetPasswordContract $user) - { - $this->putResets( - $this->getResets()->forget($user->email()) - ); - } - - public function deleteExpired() - { - $this->putResets($this->getResets()->reject(function ($item, $email) { - return $this->tokenExpired($item['created_at']); - })); - } - - public function exists(CanResetPasswordContract $user, $token) - { - $record = $this->getResets()->get($user->email()); - - return $record && - ! $this->tokenExpired(Carbon::createFromTimestamp($record['created_at'], config('app.timezone'))) - && $this->hasher->check($token, $record['token']); - } - - public function recentlyCreatedToken(CanResetPasswordContract $user) - { - $record = $this->getResets()->get($user->email()); - - return $record && parent::tokenRecentlyCreated($record['created_at']); - } - - protected function getResets() - { - if (! $this->files->exists($this->path)) { - return collect(); - } - - return collect(YAML::parse($this->files->get($this->path))); - } - - protected function putResets($resets) - { - if (! $this->files->isDirectory($dir = dirname($this->path))) { - $this->files->makeDirectory($dir); - } - - $this->files->put($this->path, YAML::dump($resets->all())); - } -} diff --git a/src/Auth/Passwords/PasswordBrokerManager.php b/src/Auth/Passwords/PasswordBrokerManager.php index 0267a1e2acd..3e70c7cd147 100644 --- a/src/Auth/Passwords/PasswordBrokerManager.php +++ b/src/Auth/Passwords/PasswordBrokerManager.php @@ -15,23 +15,12 @@ protected function createTokenRepository(array $config) $key = base64_decode(substr($key, 7)); } - if (version_compare(app()->version(), '12', '<')) { - return new TokenRepository( - $this->app['files'], - $this->app['hash'], - $config['table'], - $key, - $config['expire'], - $config['throttle'] ?? 0 - ); - } - - return new LaravelTwelveTokenRepository( + return new TokenRepository( $this->app['files'], $this->app['hash'], $config['table'], $key, - ($config['expire'] ?? 60) * 60, + $config['expire'], $config['throttle'] ?? 0 ); } diff --git a/src/Auth/Passwords/TokenRepository.php b/src/Auth/Passwords/TokenRepository.php index 0a717929fb0..3ba24434f5e 100644 --- a/src/Auth/Passwords/TokenRepository.php +++ b/src/Auth/Passwords/TokenRepository.php @@ -12,9 +12,6 @@ class TokenRepository extends DatabaseTokenRepository { protected $files; - protected $hasher; - protected $hashKey; - protected $expires; protected $path; public function __construct(Filesystem $files, HasherContract $hasher, $table, $hashKey, $expires = 60, $throttle = 60) From 80c0b897277e90fb78f623ac65d8b10db243b78f Mon Sep 17 00:00:00 2001 From: Carsten Jaksch Date: Wed, 26 Feb 2025 23:35:12 +0100 Subject: [PATCH 118/490] [5.x] Make active toolbar buttons of Bard more visible in dark mode (#11405) Make active toolbar buttons more visible in dark mode --- resources/css/components/fieldtypes/bard.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/css/components/fieldtypes/bard.css b/resources/css/components/fieldtypes/bard.css index be75f358ebd..53c1aa8121c 100644 --- a/resources/css/components/fieldtypes/bard.css +++ b/resources/css/components/fieldtypes/bard.css @@ -49,11 +49,11 @@ } .bard-toolbar-button:hover { - @apply bg-gray-200 dark:bg-dark-700; + @apply bg-gray-200 dark:bg-dark-650; } .bard-toolbar-button.active { - @apply bg-gray-300 dark:bg-dark-800 text-black dark:text-dark-100; + @apply bg-gray-300 dark:bg-dark-600 text-black dark:text-white; } .bard-toolbar-button:focus { From 9d62d6c06fbe81e67fa5c965e069da536b38c3cc Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 27 Feb 2025 09:33:11 -0500 Subject: [PATCH 119/490] [5.x] Fix target `.git` repo handling when exporting starter kit with `--clear` (#11509) --- src/StarterKits/Exporter.php | 20 +++++++++++++++++++- tests/StarterKits/ExportTest.php | 11 +++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/StarterKits/Exporter.php b/src/StarterKits/Exporter.php index f3f635d4cb4..fd2bbf0d55e 100644 --- a/src/StarterKits/Exporter.php +++ b/src/StarterKits/Exporter.php @@ -153,7 +153,9 @@ protected function clearExportPath() return $this; } - $this->files->cleanDirectory($this->exportPath); + $this->preserveGitRepository(function () { + $this->files->cleanDirectory($this->exportPath); + }); return $this; } @@ -257,4 +259,20 @@ protected function exportPackage(): self return $this; } + + /** + * Prevent filesystem callback from affecting .git repository. + */ + protected function preserveGitRepository($callback): void + { + $this->files->makeDirectory(storage_path('statamic/tmp'), 0777, true, true); + + $this->files->moveDirectory($this->exportPath.'/.git', storage_path('statamic/tmp/.git')); + + $callback(); + + $this->files->moveDirectory(storage_path('statamic/tmp/.git'), $this->exportPath.'/.git'); + + $this->files->deleteDirectory(storage_path('statamic/tmp')); + } } diff --git a/tests/StarterKits/ExportTest.php b/tests/StarterKits/ExportTest.php index 7a536a84eec..6f51a373fad 100644 --- a/tests/StarterKits/ExportTest.php +++ b/tests/StarterKits/ExportTest.php @@ -173,7 +173,11 @@ public function it_can_clear_target_export_path_with_clear_option() base_path('two'), ]); - // Imagine this exists from previous export + // Imagine we already have a target a git repo + $this->files->makeDirectory($this->targetPath('.git'), 0777, true, true); + $this->files->put($this->targetPath('.git/config'), 'Config.'); + + // And imagine this exists from previous export $this->files->makeDirectory($this->exportPath('one'), 0777, true, true); $this->files->put($this->exportPath('one/file.md'), 'One.'); @@ -195,10 +199,13 @@ public function it_can_clear_target_export_path_with_clear_option() $this->exportCoolRunnings(['--clear' => true]); - // But 'one' folder should exist after exporting with `--clear` option + // Our 'one' folder shouldn't exist after exporting with `--clear` option $this->assertFileDoesNotExist($this->exportPath('one')); $this->assertFileExists($this->exportPath('two')); + // But it should not clear `.git` directory + $this->assertFileExists($this->targetPath('.git/config')); + $this->exportCoolRunnings(); $this->cleanPaths($paths); From f27f563a9b02c25c82f8cec18d72394d584e2377 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 27 Feb 2025 14:36:36 +0000 Subject: [PATCH 120/490] [5.x] Improve validation message when handle starts with a number (#11511) --- resources/lang/en/validation.php | 1 + src/Rules/Handle.php | 7 +++++++ tests/Rules/HandleTest.php | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 3b15c72b0ab..bd70f183724 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -156,6 +156,7 @@ 'arr_fieldtype' => 'This is invalid.', 'handle' => 'Must contain only lowercase letters and numbers with underscores as separators.', + 'handle_starts_with_number' => 'Cannot start with a number.', 'slug' => 'Must contain only letters and numbers with dashes or underscores as separators.', 'code_fieldtype_rulers' => 'This is invalid.', 'composer_package' => 'Must be a valid composer package name (eg. hasselhoff/kung-fury).', diff --git a/src/Rules/Handle.php b/src/Rules/Handle.php index 9a3c06514fc..fa967c26377 100644 --- a/src/Rules/Handle.php +++ b/src/Rules/Handle.php @@ -4,11 +4,18 @@ use Closure; use Illuminate\Contracts\Validation\ValidationRule; +use Illuminate\Support\Str; class Handle implements ValidationRule { public function validate(string $attribute, mixed $value, Closure $fail): void { + if (Str::startsWith($value, range(0, 9))) { + $fail('statamic::validation.handle_starts_with_number')->translate(); + + return; + } + if (! preg_match('/^[a-zA-Z][a-zA-Z0-9]*(?:_{0,1}[a-zA-Z0-9])*$/', $value)) { $fail('statamic::validation.handle')->translate(); } diff --git a/tests/Rules/HandleTest.php b/tests/Rules/HandleTest.php index 1802979704d..f0da57878d6 100644 --- a/tests/Rules/HandleTest.php +++ b/tests/Rules/HandleTest.php @@ -43,4 +43,10 @@ public function it_outputs_helpful_validation_error() { $this->assertValidationErrorOutput(trans('statamic::validation.handle'), '_bad_input'); } + + #[Test] + public function it_outputs_helpful_validation_error_when_string_starts_with_number() + { + $this->assertValidationErrorOutput(trans('statamic::validation.handle_starts_with_number'), '1bad_input'); + } } From fbb458973cc3a2e3809d0322f000254c53bb7d6b Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 27 Feb 2025 19:39:45 -0500 Subject: [PATCH 121/490] [5.x] Query for entry origin within the same collection (#11514) --- src/Entries/Entry.php | 2 +- tests/Data/Entries/EntryTest.php | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index decfbd60945..27493c2bd4f 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -946,7 +946,7 @@ public static function __callStatic($method, $parameters) protected function getOriginByString($origin) { - return Facades\Entry::find($origin); + return $this->collection()->queryEntries()->where('id', $origin)->first(); } protected function getOriginFallbackValues() diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index f99532f921b..32b75e19f30 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\Attributes\Test; use ReflectionClass; use Statamic\Contracts\Data\Augmentable; +use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Data\AugmentedCollection; use Statamic\Entries\AugmentedEntry; use Statamic\Entries\Collection; @@ -1727,7 +1728,12 @@ public function it_gets_file_contents_for_saving_a_localized_entry() $originEntry = $this->mock(Entry::class); $originEntry->shouldReceive('id')->andReturn('123'); - Facades\Entry::shouldReceive('find')->with('123')->andReturn($originEntry); + $builder = $this->mock(QueryBuilder::class); + $builder->shouldReceive('where')->with('collection', 'test')->andReturnSelf(); + $builder->shouldReceive('where')->with('id', 123)->andReturnSelf(); + $builder->shouldReceive('first')->andReturn($originEntry); + Facades\Entry::shouldReceive('query')->andReturn($builder); + $originEntry->shouldReceive('values')->andReturn(collect([])); $originEntry->shouldReceive('blueprint')->andReturn( $this->mock(Blueprint::class)->shouldReceive('handle')->andReturn('test')->getMock() @@ -1805,13 +1811,17 @@ public function the_blueprint_is_not_added_to_the_localized_file_contents() $originEntry = $this->mock(Entry::class); $originEntry->shouldReceive('id')->andReturn('123'); - - Facades\Entry::shouldReceive('find')->with('123')->andReturn($originEntry); $originEntry->shouldReceive('values')->andReturn(collect([])); $originEntry->shouldReceive('blueprint')->andReturn( $this->mock(Blueprint::class)->shouldReceive('handle')->andReturn('another')->getMock() ); + $builder = $this->mock(QueryBuilder::class); + $builder->shouldReceive('where')->with('collection', 'test')->andReturnSelf(); + $builder->shouldReceive('where')->with('id', 123)->andReturnSelf(); + $builder->shouldReceive('first')->andReturn($originEntry); + Facades\Entry::shouldReceive('query')->andReturn($builder); + $entry = (new Entry) ->collection('test') ->origin('123'); // do not set blueprint. @@ -1831,13 +1841,17 @@ public function the_blueprint_is_added_to_the_localized_file_contents_if_explici $originEntry = $this->mock(Entry::class); $originEntry->shouldReceive('id')->andReturn('123'); - - Facades\Entry::shouldReceive('find')->with('123')->andReturn($originEntry); $originEntry->shouldReceive('values')->andReturn(collect([])); $originEntry->shouldReceive('blueprint')->andReturn( $this->mock(Blueprint::class)->shouldReceive('handle')->andReturn('another')->getMock() ); + $builder = $this->mock(QueryBuilder::class); + $builder->shouldReceive('where')->with('collection', 'test')->andReturnSelf(); + $builder->shouldReceive('where')->with('id', 123)->andReturnSelf(); + $builder->shouldReceive('first')->andReturn($originEntry); + Facades\Entry::shouldReceive('query')->andReturn($builder); + $entry = (new Entry) ->collection('test') ->origin('123') From 4c2e6adeee7e63d2b1944e92344f0b5818b18ec6 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 27 Feb 2025 20:13:50 -0500 Subject: [PATCH 122/490] changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d11289b912..67ce9a78879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Release Notes +## 5.49.1 (2025-02-27) + +### What's fixed +- Query for entry origin within the same collection [#11514](https://github.com/statamic/cms/issues/11514) by @jasonvarga +- Improve validation message when handle starts with a number [#11511](https://github.com/statamic/cms/issues/11511) by @duncanmcclean +- Fix target `.git` repo handling when exporting starter kit with `--clear` [#11509](https://github.com/statamic/cms/issues/11509) by @jesseleite +- Make active toolbar buttons of Bard more visible in dark mode [#11405](https://github.com/statamic/cms/issues/11405) by @carstenjaksch +- Alternate Laravel 12 token repository fix [#11505](https://github.com/statamic/cms/issues/11505) by @jasonvarga + + + ## 5.49.0 (2025-02-25) ### What's new From 8df1e588eca5ce5a642cd86c1510953653ba2c3a Mon Sep 17 00:00:00 2001 From: Emmanuel Beauchamps Date: Mon, 3 Mar 2025 10:40:34 +0100 Subject: [PATCH 123/490] [5.x] French translations (#11519) good for 5.49.1 --- resources/lang/fr/validation.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index d1bc7710059..033c02af5fa 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -118,6 +118,7 @@ 'uuid' => 'Doit être un UUID valide.', 'arr_fieldtype' => 'Ceci est invalide.', 'handle' => 'Peut uniquement contenir des lettres minuscules et des chiffres avec des traits de soulignement comme séparateurs.', + 'handle_starts_with_number' => 'Ne peut pas commencer par un chiffre.', 'slug' => 'Ne peut contenir que des lettres et des chiffres avec des tirets ou des traits de soulignement comme séparateurs.', 'code_fieldtype_rulers' => 'Les données saisies sont invalides.', 'composer_package' => 'Doit être un nom de paquet composer valide (ex. hasselhoff/kung-fury).', From a691c1ee1ee7c01d0fdb3ee8c60a0982dd74778a Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Mon, 3 Mar 2025 15:29:49 +0000 Subject: [PATCH 124/490] [5.x] Support `as` on nav tag (#11522) --- src/Tags/Structure.php | 8 ++++++- tests/Tags/StructureTagTest.php | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Tags/Structure.php b/src/Tags/Structure.php index 3bf57d1082a..a3a132dc9fc 100644 --- a/src/Tags/Structure.php +++ b/src/Tags/Structure.php @@ -64,7 +64,13 @@ protected function structure($handle) 'max_depth' => $this->params->get('max_depth'), ]); - return $this->toArray($tree); + $value = $this->toArray($tree); + + if ($this->parser && ($as = $this->params->get('as'))) { + return [$as => $value]; + } + + return $value; } protected function ensureStructureExists(string $handle): void diff --git a/tests/Tags/StructureTagTest.php b/tests/Tags/StructureTagTest.php index 16943397575..03baf77b66b 100644 --- a/tests/Tags/StructureTagTest.php +++ b/tests/Tags/StructureTagTest.php @@ -220,6 +220,48 @@ public function it_renders_a_nav_with_scope() ])); } + #[Test] + public function it_renders_a_nav_with_as() + { + $this->createCollectionAndNav(); + + // The html uses tags (could be any tag, but i is short) to prevent whitespace comparison issues in the assertion. + $template = <<<'EOT' +
    +{{ nav:test as="navtastic" }} +
  • Something before the loop
  • + {{ navtastic }} +
  • + {{ nav_title or title }} {{ foo }} +
  • + {{ /navtastic }} +{{ /nav:test }} +
+EOT; + + $expected = <<<'EOT' +
    +
  • Something before the loop
  • +
  • + Navtitle One bar +
  • +
  • + Two notbar +
  • +
  • + Three bar +
  • +
  • + Title only bar +
  • +
+EOT; + + $this->assertXmlStringEqualsXmlString($expected, (string) Antlers::parse($template, [ + 'foo' => 'bar', // to test that cascade is inherited. + ])); + } + #[Test] public function it_hides_unpublished_entries_by_default() { From b01b63e1f4b6a9cf0e763f7c3625397950d7e921 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 4 Mar 2025 09:34:54 -0500 Subject: [PATCH 125/490] [5.x] Icon fieldtype performance (#11523) --- .../components/fieldtypes/IconFieldtype.vue | 43 ++++++++++++++++- routes/cp.php | 2 + src/Fieldtypes/Icon.php | 16 +++++-- .../CP/Fieldtypes/IconFieldtypeController.php | 46 +++++++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 src/Http/Controllers/CP/Fieldtypes/IconFieldtypeController.php diff --git a/resources/js/components/fieldtypes/IconFieldtype.vue b/resources/js/components/fieldtypes/IconFieldtype.vue index 272d5276013..b0552816295 100644 --- a/resources/js/components/fieldtypes/IconFieldtype.vue +++ b/resources/js/components/fieldtypes/IconFieldtype.vue @@ -1,6 +1,7 @@ @@ -81,6 +81,7 @@ export default { data() { return { + requested: false, options: [], } }, @@ -98,6 +99,12 @@ export default { paginate: false, columns: 'title,id', } + }, + + noOptionsText() { + return this.typeahead && !this.requested + ? __('Start typing to search.') + : __('No options to choose from.'); } }, @@ -120,6 +127,7 @@ export default { return this.$axios.get(this.url, { params }).then(response => { this.options = response.data.data; + this.requested = true; return Promise.resolve(response); }); }, From 4f835e27d02e529f4f1ac4e7151c2f280e5ffd87 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Tue, 18 Mar 2025 23:00:37 +0100 Subject: [PATCH 143/490] [5.x] Change default value of update command selection (#11581) --- src/Search/Commands/Update.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Search/Commands/Update.php b/src/Search/Commands/Update.php index 19f448fc1a6..dba5d49f5a3 100644 --- a/src/Search/Commands/Update.php +++ b/src/Search/Commands/Update.php @@ -50,7 +50,7 @@ private function getIndexes() $selection = select( label: 'Which search index would you like to update?', options: collect(['All'])->merge($this->indexes()->keys())->all(), - default: 0 + default: 'All' ); return ($selection == 'All') ? $this->indexes() : [$this->indexes()->get($selection)]; From e87b58415ff9eea823021d92bffe4111092ee294 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 19 Mar 2025 17:43:44 +0000 Subject: [PATCH 144/490] [5.x] Prevent null data from being saved to eloquent users (#11591) --- src/Auth/Eloquent/User.php | 8 +++++++- tests/Auth/Eloquent/EloquentUserTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php index 723d5e82eae..3df06431409 100644 --- a/src/Auth/Eloquent/User.php +++ b/src/Auth/Eloquent/User.php @@ -282,6 +282,12 @@ public function set($key, $value) $value = Hash::make($value); } + if ($value === null) { + unset($this->model()->$key); + + return $this; + } + $this->model()->$key = $value; return $this; @@ -296,7 +302,7 @@ public function remove($key) public function merge($data) { - $this->data($this->data()->merge($data)); + $this->data($this->data()->merge(collect($data)->filter(fn ($v) => $v !== null)->all())); return $this; } diff --git a/tests/Auth/Eloquent/EloquentUserTest.php b/tests/Auth/Eloquent/EloquentUserTest.php index a8953f797ac..b722f18ca83 100644 --- a/tests/Auth/Eloquent/EloquentUserTest.php +++ b/tests/Auth/Eloquent/EloquentUserTest.php @@ -303,4 +303,28 @@ public function it_gets_super_correctly_on_the_model() $this->assertFalse($user->super); $this->assertFalse($user->model()->super); } + + #[Test] + public function it_does_not_save_null_values_on_the_model() + { + $user = $this->user(); + + $user->set('null_field', null); + $user->set('not_null_field', true); + + $attributes = $user->model()->getAttributes(); + + $this->assertArrayNotHasKey('null_field', $attributes); + $this->assertTrue($attributes['not_null_field']); + + $user->merge([ + 'null_field' => null, + 'not_null_field' => false, + ]); + + $attributes = $user->model()->getAttributes(); + + $this->assertArrayNotHasKey('null_field', $attributes); + $this->assertFalse($attributes['not_null_field']); + } } From 85bfbff06d6d8057a7c634e67b9f3814c1ff258a Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 19 Mar 2025 13:43:57 -0400 Subject: [PATCH 145/490] [5.x] Ability to exclude parents from nav tag (#11597) --- src/Tags/Structure.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tags/Structure.php b/src/Tags/Structure.php index a3a132dc9fc..fb29c72066f 100644 --- a/src/Tags/Structure.php +++ b/src/Tags/Structure.php @@ -136,7 +136,6 @@ public function toArray($tree, $parent = null, $depth = 1) return array_merge($data, [ 'children' => $children, - 'parent' => $parent, 'depth' => $depth, 'index' => $index, 'count' => $index + 1, @@ -145,7 +144,7 @@ public function toArray($tree, $parent = null, $depth = 1) 'is_current' => ! is_null($url) && rtrim($url, '/') === rtrim($this->currentUrl, '/'), 'is_parent' => ! is_null($url) && $this->siteAbsoluteUrl !== $absoluteUrl && URL::isAncestorOf($this->currentUrl, $url), 'is_external' => URL::isExternal((string) $absoluteUrl), - ]); + ], $this->params->bool('include_parents', true) ? ['parent' => $parent] : []); })->filter()->values(); $this->updateIsParent($pages); From 548f822f223564509a0b06d9d93f85d526920dbc Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 19 Mar 2025 17:44:55 +0000 Subject: [PATCH 146/490] [5.x] Allow revisions path to be configurable with an .env (#11594) --- config/revisions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/revisions.php b/config/revisions.php index 7ae76f05562..a755339bb14 100644 --- a/config/revisions.php +++ b/config/revisions.php @@ -25,6 +25,6 @@ | */ - 'path' => storage_path('statamic/revisions'), + 'path' => env('STATAMIC_REVISIONS_PATH', storage_path('statamic/revisions')), ]; From a74b647fe7b1ae5ade50aea4bafb0ad9a2ad13f0 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 19 Mar 2025 17:48:07 +0000 Subject: [PATCH 147/490] [5.x] Ensure toasts fired in an AssetUploaded event are delivered to front end (#11592) --- resources/js/components/assets/Uploader.vue | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/js/components/assets/Uploader.vue b/resources/js/components/assets/Uploader.vue index 6e0904d4e67..e3ffa5013a9 100644 --- a/resources/js/components/assets/Uploader.vue +++ b/resources/js/components/assets/Uploader.vue @@ -268,6 +268,8 @@ export default { handleUploadSuccess(id, response) { this.$emit('upload-complete', response.data, this.uploads); this.uploads.splice(this.findUploadIndex(id), 1); + + this.handleToasts(response._toasts ?? []); }, handleUploadError(id, status, response) { @@ -284,12 +286,19 @@ export default { msg = Object.values(response.errors)[0][0]; // Get first validation message. } } + + this.handleToasts(response._toasts ?? []); + upload.errorMessage = msg; upload.errorStatus = status; this.$emit('error', upload, this.uploads); this.processUploadQueue(); }, + handleToasts(toasts) { + toasts.forEach(toast => Statamic.$toast[toast.type](toast.message, {duration: toast.duration})); + }, + retry(id, args) { let file = this.findUpload(id).instance.form.get('file'); this.addFile(file, args); From 27eaa4e1be5a9cc12a462608513a82d8197b68c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:25:57 -0400 Subject: [PATCH 148/490] [5.x] Bump tj-actions/changed-files (#11602) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 054bf799717..92ab11fa01a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v44 + uses: tj-actions/changed-files@v46 with: files: | config @@ -116,7 +116,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v44 + uses: tj-actions/changed-files@v46 with: files: | **/*.{js,vue,ts} From b10302ba7062cf0a253b590c04224f4d83fab1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Prai=C3=9F?= <6369555+ChristianPraiss@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:26:37 +0100 Subject: [PATCH 149/490] [5.x] Fix issue with localication files named like fieldset handles (#11603) --- src/Entries/Collection.php | 2 ++ src/Extend/HasTitle.php | 2 ++ src/Git/CommitCommand.php | 2 ++ src/Http/Controllers/CP/Auth/LoginController.php | 2 ++ src/Http/Controllers/CP/Fields/FieldsController.php | 2 ++ src/Http/Controllers/CP/Fields/FieldsetController.php | 2 ++ 6 files changed, 12 insertions(+) diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index 70e5c562d42..253e05b5ec7 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -32,6 +32,8 @@ use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; +use function Statamic\trans as __; + class Collection implements Arrayable, ArrayAccess, AugmentableContract, Contract { use ContainsCascadingData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedData; diff --git a/src/Extend/HasTitle.php b/src/Extend/HasTitle.php index ddfe294d91e..21fff9b2bfd 100644 --- a/src/Extend/HasTitle.php +++ b/src/Extend/HasTitle.php @@ -4,6 +4,8 @@ use Statamic\Support\Str; +use function Statamic\trans as __; + trait HasTitle { protected static $title; diff --git a/src/Git/CommitCommand.php b/src/Git/CommitCommand.php index ff00a5de01e..cefcadd9fe2 100644 --- a/src/Git/CommitCommand.php +++ b/src/Git/CommitCommand.php @@ -6,6 +6,8 @@ use Statamic\Console\RunsInPlease; use Statamic\Facades\Git; +use function Statamic\trans as __; + class CommitCommand extends Command { use RunsInPlease; diff --git a/src/Http/Controllers/CP/Auth/LoginController.php b/src/Http/Controllers/CP/Auth/LoginController.php index 885cb5d91de..245d267a019 100644 --- a/src/Http/Controllers/CP/Auth/LoginController.php +++ b/src/Http/Controllers/CP/Auth/LoginController.php @@ -11,6 +11,8 @@ use Statamic\Http\Middleware\CP\RedirectIfAuthorized; use Statamic\Support\Str; +use function Statamic\trans as __; + class LoginController extends CpController { use ThrottlesLogins; diff --git a/src/Http/Controllers/CP/Fields/FieldsController.php b/src/Http/Controllers/CP/Fields/FieldsController.php index 2755ff8fe65..ad5afa43ebc 100644 --- a/src/Http/Controllers/CP/Fields/FieldsController.php +++ b/src/Http/Controllers/CP/Fields/FieldsController.php @@ -11,6 +11,8 @@ use Statamic\Http\Middleware\CP\CanManageBlueprints; use Statamic\Support\Str; +use function Statamic\trans as __; + class FieldsController extends CpController { public function __construct() diff --git a/src/Http/Controllers/CP/Fields/FieldsetController.php b/src/Http/Controllers/CP/Fields/FieldsetController.php index 91e7544825e..83085278d33 100644 --- a/src/Http/Controllers/CP/Fields/FieldsetController.php +++ b/src/Http/Controllers/CP/Fields/FieldsetController.php @@ -12,6 +12,8 @@ use Statamic\Support\Arr; use Statamic\Support\Str; +use function Statamic\trans as __; + class FieldsetController extends CpController { public function __construct() From ba090e88e0ce53f41487519242b8ef8cc47b7e6d Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Mar 2025 14:27:26 +0000 Subject: [PATCH 150/490] [5.x] Autoload scopes from `Query/Scopes` and `Query/Scopes/Filters` (#11601) --- src/Providers/AddonServiceProvider.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Providers/AddonServiceProvider.php b/src/Providers/AddonServiceProvider.php index 772499f989c..2743636846c 100644 --- a/src/Providers/AddonServiceProvider.php +++ b/src/Providers/AddonServiceProvider.php @@ -308,6 +308,8 @@ protected function bootScopes() { $scopes = collect($this->scopes) ->merge($this->autoloadFilesFromFolder('Scopes', Scope::class)) + ->merge($this->autoloadFilesFromFolder('Query/Scopes', Scope::class)) + ->merge($this->autoloadFilesFromFolder('Query/Scopes/Filters', Scope::class)) ->unique(); foreach ($scopes as $class) { From 52721e0d5729f8e37476ee7be25fe29a6f91d4a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:42:14 -0400 Subject: [PATCH 151/490] [5.x] Bump axios from 1.7.4 to 1.8.2 (#11604) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1cc793ed479..3096160d3b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "@tiptap/vue-2": "^2.0.2", "alpinejs": "^3.1.1", "autosize": "~3.0.12", - "axios": "^1.7.4", + "axios": "^1.8.2", "body-scroll-lock": "^4.0.0-beta.0", "codemirror": "^5.58.2", "cookies-js": "^1.2.2", @@ -3892,9 +3892,10 @@ "integrity": "sha512-xGFj5jTV4up6+fxRwtnAWiCIx/5N0tEnFn5rdhAkK1Lq2mliLMuGJgP5Bf4phck3sHGYrVKpYwugfJ61MSz9nA==" }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 7abe2d80ab5..1314a7cef3f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@tiptap/vue-2": "^2.0.2", "alpinejs": "^3.1.1", "autosize": "~3.0.12", - "axios": "^1.7.4", + "axios": "^1.8.2", "body-scroll-lock": "^4.0.0-beta.0", "codemirror": "^5.58.2", "cookies-js": "^1.2.2", From 288a770d49a0490efadc392772a989867aa1ce82 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Thu, 20 Mar 2025 21:58:25 +0100 Subject: [PATCH 152/490] [5.x] Handle collection instances in `first` modifier (#11608) --- src/Modifiers/CoreModifiers.php | 4 ++++ tests/Modifiers/FirstTest.php | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 2e8c30ec4cb..80a85714f61 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -738,6 +738,10 @@ public function first($value, $params) return Arr::first($value); } + if ($value instanceof Collection) { + return $value->first(); + } + return Stringy::first($value, Arr::get($params, 0)); } diff --git a/tests/Modifiers/FirstTest.php b/tests/Modifiers/FirstTest.php index bfdbe1400fe..1992133be1c 100644 --- a/tests/Modifiers/FirstTest.php +++ b/tests/Modifiers/FirstTest.php @@ -45,6 +45,27 @@ public static function arrayProvider() ]; } + #[Test] + #[DataProvider('collectionProvider')] + public function it_gets_the_first_value_of_a_collection($value, $expected) + { + $this->assertEquals($expected, $this->modify($value)); + } + + public static function collectionProvider() + { + return [ + 'list' => [ + collect(['alfa', 'bravo', 'charlie']), + 'alfa', + ], + 'associative' => [ + collect(['alfa' => 'bravo', 'charlie' => 'delta']), + 'bravo', + ], + ]; + } + private function modify($value, $arg = null) { return Modify::value($value)->first($arg)->fetch(); From b4b78754a5b1eb73f7eb31dd525427a2476ddcae Mon Sep 17 00:00:00 2001 From: naabster Date: Mon, 24 Mar 2025 22:03:08 +0100 Subject: [PATCH 153/490] [5.x] Escape start_page Preference to avoid invalid Redirect (#11616) --- src/Http/Controllers/CP/StartPageController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/StartPageController.php b/src/Http/Controllers/CP/StartPageController.php index 918587c30c1..e6ac7ceafa2 100644 --- a/src/Http/Controllers/CP/StartPageController.php +++ b/src/Http/Controllers/CP/StartPageController.php @@ -10,7 +10,7 @@ public function __invoke() { session()->reflash(); - $url = config('statamic.cp.route').'/'.Preference::get('start_page', config('statamic.cp.start_page')); + $url = config('statamic.cp.route').'/'.urlencode(Preference::get('start_page', config('statamic.cp.start_page'))); return redirect($url); } From 5f73b91d4c2ffeed7975f90b2a636ddd3807ad7f Mon Sep 17 00:00:00 2001 From: Guillermo Azurdia Date: Tue, 25 Mar 2025 06:01:29 -0500 Subject: [PATCH 154/490] [5.x] Spanish translations (#11617) Spanish translations --- resources/lang/es.json | 2 +- resources/lang/es/dictionary-currencies.php | 36 ++++++++++----------- resources/lang/es/fieldtypes.php | 17 ++++++---- resources/lang/es/messages.php | 2 +- resources/lang/es/permissions.php | 4 +-- 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/resources/lang/es.json b/resources/lang/es.json index 63ba945dfdb..409aebea110 100644 --- a/resources/lang/es.json +++ b/resources/lang/es.json @@ -286,7 +286,7 @@ "Current": "Actual", "Current Password": "Contraseña actual", "Current Version": "Versión actual", - "custom": "costumbre", + "custom": "personalizado", "Custom Attributes": "Atributos personalizados", "Custom Item": "Artículo personalizado", "Custom method passes": "Pases de método personalizado", diff --git a/resources/lang/es/dictionary-currencies.php b/resources/lang/es/dictionary-currencies.php index a80724aedab..71ad2d29488 100644 --- a/resources/lang/es/dictionary-currencies.php +++ b/resources/lang/es/dictionary-currencies.php @@ -2,25 +2,25 @@ return [ 'AED' => 'Dírham de los Emiratos Árabes Unidos', - 'AFN' => 'afgano afgano', + 'AFN' => 'Afgani afgano', 'ALL' => 'Lek albanés', 'AMD' => 'Dram armenio', 'ARS' => 'Peso argentino', 'AUD' => 'Dólar australiano', 'AZN' => 'Manat azerbaiyano', 'BAM' => 'Marco convertible de Bosnia y Herzegovina', - 'BDT' => 'taka bangladesí', + 'BDT' => 'Taka bangladesí', 'BGN' => 'Lev búlgaro', - 'BHD' => 'dinar bahreiní', + 'BHD' => 'Dinar bahreiní', 'BIF' => 'Franco burundiano', 'BND' => 'Dólar de Brunei', - 'BOB' => 'boliviano boliviano', + 'BOB' => 'Boliviano', 'BRL' => 'Real brasileño', 'BWP' => 'Lluvia en Botsuana', 'BYN' => 'Rublo bielorruso', 'BZD' => 'Dólar beliceño', 'CAD' => 'Dólar canadiense', - 'CDF' => 'francés congoleño', + 'CDF' => 'Francés congoleño', 'CHF' => 'Franco suizo', 'CLP' => 'Peso chileno', 'CNY' => 'Yuan chino', @@ -32,24 +32,24 @@ 'DKK' => 'Corona danesa', 'DOP' => 'Peso dominicano', 'DZD' => 'Dinar argelino', - 'EEK' => 'corona estonia', + 'EEK' => 'Corona estonia', 'EGP' => 'Libra egipcia', 'ERN' => 'Nakfa eritrea', 'ETB' => 'Birr etíope', 'EUR' => 'Euro', 'GBP' => 'Libra esterlina británica', 'GEL' => 'Lari georgiano', - 'GHS' => 'cedi ghanés', + 'GHS' => 'Cedi ghanés', 'GNF' => 'Franco guineano', 'GTQ' => 'Quetzal guatemalteco', 'HKD' => 'Dólar de Hong Kong', 'HNL' => 'Lempira hondureña', - 'HRK' => 'kuna croata', - 'HUF' => 'florín húngaro', + 'HRK' => 'Kuna croata', + 'HUF' => 'Florín húngaro', 'IDR' => 'Rupia indonesia', 'ILS' => 'Nuevo séquel israelí', 'INR' => 'Rupia india', - 'IQD' => 'dinar iraquí', + 'IQD' => 'Dinar iraquí', 'IRR' => 'Rial iraní', 'ISK' => 'Kr\\u00f3na islandés', 'JMD' => 'Dólar jamaiquino', @@ -77,19 +77,19 @@ 'MYR' => 'Ringgit malayo', 'MZN' => 'Metical mozambiqueño', 'NAD' => 'Dólar de Namibia', - 'NGN' => 'naira nigeriana', + 'NGN' => 'Naira nigeriana', 'NIO' => 'Córdoba nicaragüense', 'NOK' => 'Corona noruega', 'NPR' => 'Rupia nepalí', 'NZD' => 'Dólar neozelandés', 'OMR' => 'Rial omaní', 'PAB' => 'Balboa panameño', - 'PEN' => 'Peruvian Nuevo Sol', - 'PHP' => 'peso filipino', + 'PEN' => 'Sol peruano', + 'PHP' => 'Peso filipino', 'PKR' => 'Rupia paquistaní', 'PLN' => 'Zloty polaco', - 'PYG' => 'guaraní paraguayo', - 'QAR' => 'rial qatarí', + 'PYG' => 'Guaraní paraguayo', + 'QAR' => 'Rial qatarí', 'RON' => 'Leu rumano', 'RSD' => 'Dinar serbio', 'RUB' => 'Rublo ruso', @@ -102,15 +102,15 @@ 'SYP' => 'Libra siria', 'THB' => 'Baht tailandés', 'TND' => 'Dinar tunecino', - 'TOP' => 'pa\\u02bbanga tongano', + 'TOP' => 'Pa\\u02bbanga tongano', 'TRY' => 'Lira turca', 'TTD' => 'Dólar de Trinidad y Tobago', 'TWD' => 'Nuevo dólar de Taiwán', 'TZS' => 'Chelín tanzano', - 'UAH' => 'grivna ucraniana', + 'UAH' => 'Grivna ucraniana', 'UGX' => 'Chelín ugandés', 'USD' => 'Dólar estadounidense', - 'UYU' => 'Uruguayan Peso', + 'UYU' => 'Peso uruguayo', 'UZS' => 'Som de Uzbekistán', 'VEF' => 'Bol\\u000Edvar venezolano', 'VND' => 'Dong vietnamita', diff --git a/resources/lang/es/fieldtypes.php b/resources/lang/es/fieldtypes.php index 4fc0b67990a..9c00b675519 100644 --- a/resources/lang/es/fieldtypes.php +++ b/resources/lang/es/fieldtypes.php @@ -21,7 +21,8 @@ 'assets.config.show_set_alt' => 'Muestra un enlace para establecer el texto alternativo de cualquier imagen.', 'assets.dynamic_folder_pending_field' => 'Este campo estará disponible una vez que se configure :field .', 'assets.dynamic_folder_pending_save' => 'Este campo estará disponible después de guardar.', - 'assets.title' => 'Assets', + 'assets.title' => 'Medios', + 'asset_folders.config.container' => 'Elige qué contenedor de medios usar para este campo.', 'bard.config.allow_source' => 'Activa la opción para ver el código fuente HTML mientras escribes.', 'bard.config.always_show_set_button' => 'Habilita para mostrar siempre el botón "Agregar conjunto".', 'bard.config.buttons' => 'Elige qué botones mostrar en la barra de herramientas.', @@ -48,7 +49,7 @@ 'bard.config.toolbar_mode' => 'Elige qué estilo de barra de herramientas prefieres.', 'bard.config.word_count' => 'Muestra el recuento de palabras en la parte inferior del campo.', 'bard.title' => 'Bard', - 'button_group.title' => 'Button Group', + 'button_group.title' => 'Grupo de botones', 'checkboxes.config.inline' => 'Mostrar los botones de checkbox en una fila.', 'checkboxes.config.options' => 'Establece las llaves del arreglo y sus etiquetas opcionales.', 'checkboxes.title' => 'Checkboxes', @@ -98,7 +99,7 @@ 'grid.config.min_rows' => 'Establecer un número mínimo de filas.', 'grid.config.mode' => 'Elige tu estilo preferido.', 'grid.config.reorderable' => 'Habilita para permitir el reordenamiento de filas.', - 'grid.title' => 'Grid', + 'grid.title' => 'Cuadrícula', 'group.config.fields' => 'Configure los campos que se anidarán dentro de este grupo.', 'group.title' => 'Grupo', 'hidden.title' => 'Oculto', @@ -110,7 +111,7 @@ 'integer.title' => 'Integer', 'link.config.collections' => 'Las entradas de estas colecciones estarán disponibles. Si deja esto en blanco, las entradas de las colecciones enrutables estarán disponibles.', 'link.config.container' => 'Elige qué contenedor de activos usar para este campo.', - 'link.title' => 'Link', + 'link.title' => 'Enlace', 'list.title' => 'Lista', 'markdown.config.automatic_line_breaks' => 'Permite saltos de línea automáticos.', 'markdown.config.automatic_links' => 'Permite la vinculación automática de cualquier URL.', @@ -161,13 +162,15 @@ 'select.config.push_tags' => 'Agregar etiquetas recién creadas a la lista de opciones.', 'select.config.searchable' => 'Habilita la búsqueda a través de las opciones posibles.', 'select.config.taggable' => 'Permitir agregar nuevas opciones además de las opciones predefinidas', - 'select.title' => 'Select', + 'select.title' => 'Selección', 'sites.title' => 'Sitios', 'slug.config.from' => 'Campo de destino desde el cual crear un slug.', 'slug.config.generate' => 'Crea un slug automáticamente a partir del campo `from` de destino.', 'slug.config.show_regenerate' => 'Muestra el botón de regeneración para volver a slugificar desde el campo de destino.', 'slug.title' => 'Slug', 'structures.title' => 'Estructuras', + 'table.config.max_columns' => 'Establecer un número máximo de columnas.', + 'table.config.max_rows' => 'Establecer un número máximo de filas.', 'table.title' => 'Tablas', 'taggable.config.options' => 'Proporciona etiquetas predefinidas que se pueden seleccionar.', 'taggable.config.placeholder' => 'Escribe y pulsa ↩ Enter', @@ -191,9 +194,9 @@ 'textarea.title' => 'Textarea', 'time.config.seconds_enabled' => 'Mostrar segundos en el selector de tiempo.', 'time.title' => 'Tiempo', - 'toggle.config.inline_label' => 'Establezca una etiqueta en línea que se muestre junto a la entrada de toggle.', + 'toggle.config.inline_label' => 'Establezca una etiqueta en línea que se muestre junto a la entrada de interruptor.', 'toggle.config.inline_label_when_true' => 'Establezca una etiqueta en línea que se mostrará cuando el valor del interruptor sea verdadero.', - 'toggle.title' => 'Toggle', + 'toggle.title' => 'Interruptor', 'user_groups.title' => 'Grupos de Usuarios', 'user_roles.title' => 'Roles de Usuario', 'users.config.query_scopes' => 'Elija qué ámbitos de consulta se deben aplicar al recuperar usuarios seleccionables.', diff --git a/resources/lang/es/messages.php b/resources/lang/es/messages.php index fd457f195f8..dd82d205935 100644 --- a/resources/lang/es/messages.php +++ b/resources/lang/es/messages.php @@ -252,7 +252,7 @@ 'user_wizard_invitation_share_before' => 'Después de crear el usuario, se te darán detalles para compartir :email través de tu método preferido.', 'user_wizard_invitation_subject' => 'Activa tu nueva cuenta de Statamic en :site', 'user_wizard_roles_groups_intro' => 'Los usuarios pueden ser asignados a roles que personalizan sus permisos, acceso y habilidades en todo el Panel de Control.', - 'user_wizard_super_admin_instructions' => 'Los superadministradores tienen control y acceso completos a todo en el Panel de Control. Concede este rol sabiamente.', + 'user_wizard_super_admin_instructions' => 'Los super-administradores tienen control y acceso completos a todo en el Panel de Control. Concede este rol sabiamente.', 'view_more_count' => 'Ver :count más', 'width_x_height' => ':width x :height', ]; diff --git a/resources/lang/es/permissions.php b/resources/lang/es/permissions.php index 3516cfbc512..966fc68edbb 100644 --- a/resources/lang/es/permissions.php +++ b/resources/lang/es/permissions.php @@ -81,8 +81,8 @@ 'access_utility' => ':title', 'access_utility_desc' => 'Otorga acceso a la utilidad :title', 'group_misc' => 'Varios', - 'resolve_duplicate_ids' => 'Resolver identificaciones duplicadas', - 'resolve_duplicate_ids_desc' => 'Otorga la capacidad de ver y resolver identificaciones duplicadas.', + 'resolve_duplicate_ids' => 'Resolver identificadores duplicados', + 'resolve_duplicate_ids_desc' => 'Otorga la capacidad de ver y resolver identificadores duplicados.', 'view_graphql' => 'Ver GraphQL', 'view_graphql_desc' => 'Otorga la capacidad de acceder al visor GraphQL', ]; From 502b8d3e21de3e6c2296d5ccd2ca76d35cab261b Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 25 Mar 2025 09:36:19 -0400 Subject: [PATCH 155/490] [5.x] Fix icon fieldtype in nav builder (#11618) --- resources/js/components/nav/ItemEditor.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/nav/ItemEditor.vue b/resources/js/components/nav/ItemEditor.vue index 9477b6e51c2..2f69e9ada90 100644 --- a/resources/js/components/nav/ItemEditor.vue +++ b/resources/js/components/nav/ItemEditor.vue @@ -41,9 +41,9 @@ - + From c3d7529d0e1a1f43cd2338526953454c77889ca9 Mon Sep 17 00:00:00 2001 From: Kevin Meijer Date: Tue, 25 Mar 2025 14:42:13 +0100 Subject: [PATCH 156/490] [5.x] Added option to exclude asset containers from generating presets (#11613) --- src/Console/Commands/AssetsGeneratePresets.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Console/Commands/AssetsGeneratePresets.php b/src/Console/Commands/AssetsGeneratePresets.php index aba0a593f9b..b2e4ce89136 100644 --- a/src/Console/Commands/AssetsGeneratePresets.php +++ b/src/Console/Commands/AssetsGeneratePresets.php @@ -24,7 +24,9 @@ class AssetsGeneratePresets extends Command * * @var string */ - protected $signature = 'statamic:assets:generate-presets {--queue : Queue the image generation.}'; + protected $signature = 'statamic:assets:generate-presets + {--queue : Queue the image generation.} + {--excluded-containers= : Comma separated list of container handles to exclude.}'; /** * The console command description. @@ -55,7 +57,15 @@ public function handle() $this->shouldQueue = false; } - AssetContainer::all()->sortBy('title')->each(function ($container) { + $excludedContainers = $this->option('excluded-containers'); + + if ($excludedContainers) { + $excludedContainers = explode(',', $excludedContainers); + } + + AssetContainer::all()->filter(function ($container) use ($excludedContainers) { + return ! in_array($container->handle(), $excludedContainers ?? []); + })->sortBy('title')->each(function ($container) { note('Generating presets for '.$container->title().'...'); $this->generatePresets($container); $this->newLine(); From 6223436ce0c2ec3d724a7f7310579a6b0f7f9edc Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 25 Mar 2025 19:08:32 +0000 Subject: [PATCH 157/490] [5.x] Support query scopes in REST API (#10893) Co-authored-by: Jason Varga --- src/API/FilterAuthorizer.php | 12 ++-- src/API/QueryScopeAuthorizer.php | 8 +++ src/Http/Controllers/API/ApiController.php | 62 +++++++++++++++++++ src/Http/Controllers/API/AssetsController.php | 8 ++- .../API/CollectionEntriesController.php | 8 ++- .../API/CollectionTreeController.php | 6 ++ .../API/TaxonomyTermEntriesController.php | 8 ++- .../API/TaxonomyTermsController.php | 8 ++- src/Http/Controllers/API/UsersController.php | 8 ++- tests/API/APITest.php | 28 +++++++++ 10 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 src/API/QueryScopeAuthorizer.php diff --git a/src/API/FilterAuthorizer.php b/src/API/FilterAuthorizer.php index 2a7f6e90078..c66f77dce6f 100644 --- a/src/API/FilterAuthorizer.php +++ b/src/API/FilterAuthorizer.php @@ -6,6 +6,8 @@ class FilterAuthorizer extends AbstractAuthorizer { + protected $configKey = 'allowed_filters'; + /** * Get allowed filters for resource. * @@ -17,7 +19,7 @@ class FilterAuthorizer extends AbstractAuthorizer */ public function allowedForResource($configFile, $queriedResource) { - $config = config("statamic.{$configFile}.resources.{$queriedResource}.allowed_filters"); + $config = config("statamic.{$configFile}.resources.{$queriedResource}.{$this->configKey}"); // Use explicitly configured `allowed_filters` array, otherwise no filters should be allowed. return is_array($config) @@ -54,7 +56,7 @@ public function allowedForSubResources($configFile, $queriedResource, $queriedHa // Determine if any of our queried resources have filters explicitly disabled. $disabled = $resources - ->filter(fn ($resource) => Arr::get($config, "{$resource}.allowed_filters") === false) + ->filter(fn ($resource) => Arr::get($config, "{$resource}.{$this->configKey}") === false) ->isNotEmpty(); // If any queried resource is explicitly disabled, then no filters should be allowed. @@ -65,10 +67,10 @@ public function allowedForSubResources($configFile, $queriedResource, $queriedHa // Determine `allowed_filters` by filtering out any that don't appear in all of them. // A resource named `*` will apply to all enabled resources at once. return $resources - ->map(fn ($resource) => $config[$resource]['allowed_filters'] ?? []) + ->map(fn ($resource) => $config[$resource][$this->configKey] ?? []) ->reduce(function ($carry, $allowedFilters) use ($config) { - return $carry->intersect($allowedFilters)->merge($config['*']['allowed_filters'] ?? []); - }, collect($config[$resources[0] ?? '']['allowed_filters'] ?? [])) + return $carry->intersect($allowedFilters)->merge($config['*'][$this->configKey] ?? []); + }, collect($config[$resources[0] ?? ''][$this->configKey] ?? [])) ->all(); } } diff --git a/src/API/QueryScopeAuthorizer.php b/src/API/QueryScopeAuthorizer.php new file mode 100644 index 00000000000..91b5c985046 --- /dev/null +++ b/src/API/QueryScopeAuthorizer.php @@ -0,0 +1,8 @@ +updateAndPaginate($query); + } + + /** + * Filter, sort, scope, and paginate query for API resource output. + * + * @param \Statamic\Query\Builder $query + * @return \Statamic\Extensions\Pagination\LengthAwarePaginator + */ + protected function updateAndPaginate($query) { return $this ->filter($query) ->sort($query) + ->scope($query) ->paginate($query); } @@ -171,6 +187,52 @@ protected function doesntHaveFilter($field) ->contains($field); } + /** + * Apply query scopes a query based on conditions in the query_scope parameter. + * + * /endpoint?query_scope[scope_handle]=foo&query_scope[another_scope]=bar + * + * @param \Statamic\Query\Builder $query + * @return $this + */ + protected function scope($query) + { + $this->getScopes() + ->each(function ($value, $handle) use ($query) { + Scope::find($handle)?->apply($query, Arr::wrap($value)); + }); + + return $this; + } + + /** + * Get scopes for querying. + * + * @return \Illuminate\Support\Collection + */ + protected function getScopes() + { + if (! method_exists($this, 'allowedQueryScopes')) { + return collect(); + } + + $scopes = collect(request()->query_scope ?? []); + + $allowedScopes = collect($this->allowedQueryScopes()); + + $forbidden = $scopes + ->keys() + ->filter(fn ($handle) => ! Scope::find($handle) || ! $allowedScopes->contains($handle)); + + if ($forbidden->isNotEmpty()) { + throw ApiValidationException::withMessages([ + 'query_scope' => Str::plural('Forbidden query scope', $forbidden).': '.$forbidden->join(', '), + ]); + } + + return $scopes; + } + /** * Sorts the query based on the sort parameter. * diff --git a/src/Http/Controllers/API/AssetsController.php b/src/Http/Controllers/API/AssetsController.php index 9d144b4834f..3fc9ebc61f1 100644 --- a/src/Http/Controllers/API/AssetsController.php +++ b/src/Http/Controllers/API/AssetsController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\API; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Statamic\Http\Resources\API\AssetResource; class AssetsController extends ApiController @@ -22,7 +23,7 @@ public function index($assetContainer) ->filter->isRelationship()->keys()->all(); return app(AssetResource::class)::collection( - $this->filterSortAndPaginate($assetContainer->queryAssets()->with($with)) + $this->updateAndPaginate($assetContainer->queryAssets()->with($with)) ); } @@ -37,4 +38,9 @@ protected function allowedFilters() { return FilterAuthorizer::allowedForSubResources('api', 'assets', $this->containerHandle); } + + protected function allowedQueryScopes() + { + return QueryScopeAuthorizer::allowedForSubResources('api', 'assets', $this->containerHandle); + } } diff --git a/src/Http/Controllers/API/CollectionEntriesController.php b/src/Http/Controllers/API/CollectionEntriesController.php index 892d823c966..13b3d7c73b8 100644 --- a/src/Http/Controllers/API/CollectionEntriesController.php +++ b/src/Http/Controllers/API/CollectionEntriesController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\API; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\Entry; use Statamic\Http\Resources\API\EntryResource; @@ -29,7 +30,7 @@ public function index($collection) ->filter->isRelationship()->keys()->all(); return app(EntryResource::class)::collection( - $this->filterSortAndPaginate($collection->queryEntries()->with($with)) + $this->updateAndPaginate($collection->queryEntries()->with($with)) ); } @@ -81,4 +82,9 @@ protected function allowedFilters() { return FilterAuthorizer::allowedForSubResources('api', 'collections', $this->collectionHandle); } + + protected function allowedQueryScopes() + { + return QueryScopeAuthorizer::allowedForSubResources('api', 'collections', $this->collectionHandle); + } } diff --git a/src/Http/Controllers/API/CollectionTreeController.php b/src/Http/Controllers/API/CollectionTreeController.php index 9a58dbbe723..2a5cd9f0e9d 100644 --- a/src/Http/Controllers/API/CollectionTreeController.php +++ b/src/Http/Controllers/API/CollectionTreeController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\API; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Statamic\Exceptions\NotFoundHttpException; use Statamic\Http\Resources\API\TreeResource; use Statamic\Query\ItemQueryBuilder; @@ -48,4 +49,9 @@ protected function allowedFilters() { return FilterAuthorizer::allowedForSubResources('api', 'collections', $this->collectionHandle); } + + protected function allowedQueryScopes() + { + return QueryScopeAuthorizer::allowedForSubResources('api', 'collections', $this->collectionHandle); + } } diff --git a/src/Http/Controllers/API/TaxonomyTermEntriesController.php b/src/Http/Controllers/API/TaxonomyTermEntriesController.php index 7b732c99205..cf92e8b3e2b 100644 --- a/src/Http/Controllers/API/TaxonomyTermEntriesController.php +++ b/src/Http/Controllers/API/TaxonomyTermEntriesController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\API; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Facades\Statamic\API\ResourceAuthorizer; use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\Collection; @@ -46,7 +47,7 @@ public function index($taxonomy, $term) $with = $this->getRelationshipFieldsFromCollections($taxonomy); return app(EntryResource::class)::collection( - $this->filterSortAndPaginate($query->with($with)) + $this->updateAndPaginate($query->with($with)) ); } @@ -72,4 +73,9 @@ protected function allowedFilters() { return FilterAuthorizer::allowedForSubResources('api', 'collections', $this->allowedCollections); } + + protected function allowedQueryScopes() + { + return QueryScopeAuthorizer::allowedForSubResources('api', 'collections', $this->allowedCollections); + } } diff --git a/src/Http/Controllers/API/TaxonomyTermsController.php b/src/Http/Controllers/API/TaxonomyTermsController.php index b97b50f96f5..93d40fa860b 100644 --- a/src/Http/Controllers/API/TaxonomyTermsController.php +++ b/src/Http/Controllers/API/TaxonomyTermsController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\API; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\Term; use Statamic\Http\Resources\API\TermResource; @@ -24,7 +25,7 @@ public function index($taxonomy) ->filter->isRelationship()->keys()->all(); return app(TermResource::class)::collection( - $this->filterSortAndPaginate($taxonomy->queryTerms()->with($with)) + $this->updateAndPaginate($taxonomy->queryTerms()->with($with)) ); } @@ -43,4 +44,9 @@ protected function allowedFilters() { return FilterAuthorizer::allowedForSubResources('api', 'taxonomies', $this->taxonomyHandle); } + + protected function allowedQueryScopes() + { + return QueryScopeAuthorizer::allowedForSubResources('api', 'taxonomies', $this->taxonomyHandle); + } } diff --git a/src/Http/Controllers/API/UsersController.php b/src/Http/Controllers/API/UsersController.php index de0a0a2f899..25d9ca367e9 100644 --- a/src/Http/Controllers/API/UsersController.php +++ b/src/Http/Controllers/API/UsersController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\API; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\User; use Statamic\Http\Resources\API\UserResource; @@ -16,7 +17,7 @@ public function index() $this->abortIfDisabled(); return app(UserResource::class)::collection( - $this->filterSortAndPaginate(User::query()) + $this->updateAndPaginate(User::query()) ); } @@ -42,4 +43,9 @@ protected function allowedFilters() ->reject(fn ($field) => in_array($field, ['password', 'password_hash'])) ->all(); } + + protected function allowedQueryScopes() + { + return QueryScopeAuthorizer::allowedForResource('api', 'users'); + } } diff --git a/tests/API/APITest.php b/tests/API/APITest.php index 32ba32298bf..d9cb6813f24 100644 --- a/tests/API/APITest.php +++ b/tests/API/APITest.php @@ -10,6 +10,7 @@ use Statamic\Facades\Blueprint; use Statamic\Facades\Token; use Statamic\Facades\User; +use Statamic\Query\Scopes\Scope; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -136,6 +137,25 @@ public function it_filters_out_past_entries_from_past_private_collection() $response->assertJsonPath('data.0.id', 'a'); } + #[Test] + public function it_can_use_a_query_scope_on_collection_entries_when_configuration_allows_for_it() + { + app('statamic.scopes')['test_scope'] = TestScope::class; + + Facades\Config::set('statamic.api.resources.collections.pages', [ + 'allowed_query_scopes' => ['test_scope'], + ]); + + Facades\Collection::make('pages')->save(); + + Facades\Entry::make()->collection('pages')->id('about')->slug('about')->published(true)->save(); + Facades\Entry::make()->collection('pages')->id('dance')->slug('dance')->published(true)->save(); + Facades\Entry::make()->collection('pages')->id('nectar')->slug('nectar')->published(true)->save(); + + $this->assertEndpointDataCount('/api/collections/pages/entries?query_scope[test_scope][operator]=is&query_scope[test_scope][value]=about', 1); + $this->assertEndpointDataCount('/api/collections/pages/entries?query_scope[test_scope][operator]=isnt&query_scope[test_scope][value]=about', 2); + } + #[Test] public function it_can_filter_collection_entries_when_configuration_allows_for_it() { @@ -624,3 +644,11 @@ public function handle(\Statamic\Contracts\Tokens\Token $token, \Illuminate\Http return $next($token); } } + +class TestScope extends Scope +{ + public function apply($query, $values) + { + $query->where('id', $values['operator'] == 'is' ? '=' : '!=', $values['value']); + } +} From f09856691c58d669ab2b36bcf8167e88fcbe1144 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 25 Mar 2025 19:49:28 +0000 Subject: [PATCH 158/490] [5.x] Support query scopes in GraphQL (#11533) Co-authored-by: Jason Varga --- .../Middleware/AuthorizeQueryScopes.php | 28 +++++ src/GraphQL/Queries/Concerns/ScopesQuery.php | 24 +++++ src/GraphQL/Queries/EntriesQuery.php | 14 +++ tests/Feature/GraphQL/EntriesTest.php | 100 ++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 src/GraphQL/Middleware/AuthorizeQueryScopes.php create mode 100644 src/GraphQL/Queries/Concerns/ScopesQuery.php diff --git a/src/GraphQL/Middleware/AuthorizeQueryScopes.php b/src/GraphQL/Middleware/AuthorizeQueryScopes.php new file mode 100644 index 00000000000..0d442894317 --- /dev/null +++ b/src/GraphQL/Middleware/AuthorizeQueryScopes.php @@ -0,0 +1,28 @@ +allowedScopes($args)); + + $forbidden = collect($args['query_scope'] ?? []) + ->keys() + ->filter(fn ($filter) => ! $allowedScopes->contains($filter)); + + if ($forbidden->isNotEmpty()) { + throw ValidationException::withMessages([ + 'query_scope' => 'Forbidden: '.$forbidden->join(', '), + ]); + } + + return $next($root, $args, $context, $info); + } +} diff --git a/src/GraphQL/Queries/Concerns/ScopesQuery.php b/src/GraphQL/Queries/Concerns/ScopesQuery.php new file mode 100644 index 00000000000..97e33053e1a --- /dev/null +++ b/src/GraphQL/Queries/Concerns/ScopesQuery.php @@ -0,0 +1,24 @@ + $value) { + Scope::find($handle)?->apply($query, Arr::wrap($value)); + } + } +} diff --git a/src/GraphQL/Queries/EntriesQuery.php b/src/GraphQL/Queries/EntriesQuery.php index 6b2dfe6c1bd..434ec3cf22f 100644 --- a/src/GraphQL/Queries/EntriesQuery.php +++ b/src/GraphQL/Queries/EntriesQuery.php @@ -3,14 +3,17 @@ namespace Statamic\GraphQL\Queries; use Facades\Statamic\API\FilterAuthorizer; +use Facades\Statamic\API\QueryScopeAuthorizer; use Facades\Statamic\API\ResourceAuthorizer; use GraphQL\Type\Definition\Type; use Statamic\Facades\Entry; use Statamic\Facades\GraphQL; use Statamic\GraphQL\Middleware\AuthorizeFilters; +use Statamic\GraphQL\Middleware\AuthorizeQueryScopes; use Statamic\GraphQL\Middleware\AuthorizeSubResources; use Statamic\GraphQL\Middleware\ResolvePage; use Statamic\GraphQL\Queries\Concerns\FiltersQuery; +use Statamic\GraphQL\Queries\Concerns\ScopesQuery; use Statamic\GraphQL\Types\EntryInterface; use Statamic\GraphQL\Types\JsonArgument; use Statamic\Support\Str; @@ -21,6 +24,8 @@ class EntriesQuery extends Query filterQuery as traitFilterQuery; } + use ScopesQuery; + protected $attributes = [ 'name' => 'entries', ]; @@ -29,6 +34,7 @@ class EntriesQuery extends Query AuthorizeSubResources::class, ResolvePage::class, AuthorizeFilters::class, + AuthorizeQueryScopes::class, ]; public function type(): Type @@ -43,6 +49,7 @@ public function args(): array 'limit' => GraphQL::int(), 'page' => GraphQL::int(), 'filter' => GraphQL::type(JsonArgument::NAME), + 'query_scope' => GraphQL::type(JsonArgument::NAME), 'sort' => GraphQL::listOf(GraphQL::string()), 'site' => GraphQL::string(), ]; @@ -60,6 +67,8 @@ public function resolve($root, $args) $this->filterQuery($query, $args['filter'] ?? []); + $this->scopeQuery($query, $args['query_scope'] ?? []); + $this->sortQuery($query, $args['sort'] ?? []); return $query->paginate($args['limit'] ?? 1000); @@ -105,4 +114,9 @@ public function allowedFilters($args) { return FilterAuthorizer::allowedForSubResources('graphql', 'collections', $args['collection'] ?? '*'); } + + public function allowedScopes($args) + { + return QueryScopeAuthorizer::allowedForSubResources('graphql', 'collections', $args['collection'] ?? '*'); + } } diff --git a/tests/Feature/GraphQL/EntriesTest.php b/tests/Feature/GraphQL/EntriesTest.php index 93fca8c4ee8..e69e57368da 100644 --- a/tests/Feature/GraphQL/EntriesTest.php +++ b/tests/Feature/GraphQL/EntriesTest.php @@ -11,6 +11,7 @@ use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; +use Statamic\Query\Scopes\Scope; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -949,4 +950,103 @@ public function it_filters_out_past_entries_from_past_private_collection() ['id' => 'a'], ]]]]); } + + #[Test] + public function it_cannot_use_query_scopes_by_default() + { + $this->createEntries(); + + $query = <<<'GQL' +{ + entries(query_scope: { test_scope: { operator: "is", value: 1 }}) { + data { + id + title + } + } +} +GQL; + + $this + ->withoutExceptionHandling() + ->post('/graphql', ['query' => $query]) + ->assertJson([ + 'errors' => [[ + 'message' => 'validation', + 'extensions' => [ + 'validation' => [ + 'query_scope' => ['Forbidden: test_scope'], + ], + ], + ]], + 'data' => [ + 'entries' => null, + ], + ]); + } + + #[Test] + public function it_can_use_a_query_scope_when_configuration_allows_for_it() + { + $this->createEntries(); + + ResourceAuthorizer::shouldReceive('isAllowed')->with('graphql', 'collections')->andReturnTrue(); + ResourceAuthorizer::shouldReceive('allowedSubResources')->with('graphql', 'collections')->andReturn(Collection::handles()->all()); + ResourceAuthorizer::makePartial(); + + app('statamic.scopes')['test_scope'] = TestScope::class; + + config()->set('statamic.graphql.resources.collections.pages', [ + 'allowed_query_scopes' => ['test_scope'], + ]); + + $query = <<<'GQL' +{ + entries(query_scope: { test_scope: { operator: "is", value: 1 }}) { + data { + id + title + } + } +} +GQL; + + $this + ->withoutExceptionHandling() + ->post('/graphql', ['query' => $query]) + ->assertGqlOk() + ->assertExactJson(['data' => ['entries' => ['data' => [ + ['id' => '1', 'title' => 'Standard Blog Post'], + ]]]]); + + $query = <<<'GQL' +{ + entries(query_scope: { test_scope: { operator: "isnt", value: 1 }}) { + data { + id + title + } + } +} +GQL; + + $this + ->withoutExceptionHandling() + ->post('/graphql', ['query' => $query]) + ->assertGqlOk() + ->assertExactJson(['data' => ['entries' => ['data' => [ + ['id' => '2', 'title' => 'Art Directed Blog Post'], + ['id' => '3', 'title' => 'Event One'], + ['id' => '4', 'title' => 'Event Two'], + ['id' => '5', 'title' => 'Hamburger'], + ]]]]); + } +} + +class TestScope extends Scope +{ + public function apply($query, $values) + { + $query->where('id', $values['operator'] == 'is' ? '=' : '!=', $values['value']); + } } From fbb3ad494247a1b7a68840aeb2a74148c39416d2 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 25 Mar 2025 16:59:32 -0400 Subject: [PATCH 159/490] changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 488b8035d15..2814159be88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Release Notes +## 5.52.0 (2025-03-25) + +### What's new +- Support query scopes in GraphQL [#11533](https://github.com/statamic/cms/issues/11533) by @ryanmitchell +- Support query scopes in REST API [#10893](https://github.com/statamic/cms/issues/10893) by @ryanmitchell +- Added option to exclude asset containers from generating presets [#11613](https://github.com/statamic/cms/issues/11613) by @kevinmeijer97 +- Handle collection instances in `first` modifier [#11608](https://github.com/statamic/cms/issues/11608) by @marcorieser +- Autoload scopes from `Query/Scopes` and `Query/Scopes/Filters` [#11601](https://github.com/statamic/cms/issues/11601) by @duncanmcclean +- Allow revisions path to be configurable with an .env [#11594](https://github.com/statamic/cms/issues/11594) by @ryanmitchell +- Ability to exclude parents from nav tag [#11597](https://github.com/statamic/cms/issues/11597) by @jasonvarga +- Allow a custom asset meta cache store to be specified [#11512](https://github.com/statamic/cms/issues/11512) by @ryanmitchell + +### What's fixed +- Fix icon fieldtype in nav builder [#11618](https://github.com/statamic/cms/issues/11618) by @jasonvarga +- Escape start_page Preference to avoid invalid Redirect [#11616](https://github.com/statamic/cms/issues/11616) by @naabster +- Fix issue with localization files named like fieldset handles [#11603](https://github.com/statamic/cms/issues/11603) by @ChristianPraiss +- Ensure toasts fired in an AssetUploaded event are delivered to front end [#11592](https://github.com/statamic/cms/issues/11592) by @ryanmitchell +- Prevent null data from being saved to eloquent users [#11591](https://github.com/statamic/cms/issues/11591) by @ryanmitchell +- Change default value of update command selection [#11581](https://github.com/statamic/cms/issues/11581) by @Jade-GG +- Adjust relationship field typeahead no-options message [#11590](https://github.com/statamic/cms/issues/11590) by @jasonvarga +- Bump axios from 1.7.4 to 1.8.2 [#11604](https://github.com/statamic/cms/issues/11604) by @dependabot +- Bump tj-actions/changed-files [#11602](https://github.com/statamic/cms/issues/11602) by @dependabot +- Spanish translations [#11617](https://github.com/statamic/cms/issues/11617) by @nopticon + + + ## 5.51.0 (2025-03-17) ### What's new From 84d76043455ef6ac3bc70caa14ac5b77b043dd40 Mon Sep 17 00:00:00 2001 From: Emmanuel Beauchamps Date: Wed, 26 Mar 2025 11:19:02 +0100 Subject: [PATCH 160/490] [5.x] French translations (#11622) --- resources/lang/fr.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/fr.json b/resources/lang/fr.json index fc8e148f360..ca46432e9a4 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -912,6 +912,7 @@ "Stacked": "Empilé", "Start Impersonating": "Usurper cette identité", "Start Page": "Page de démarrage", + "Start typing to search.": "Commencer à saisir pour rechercher.", "Statamic": "Statamic", "Statamic Pro is required.": "Statamic Pro est requis.", "Static Page Cache": "Cache de page statique", From b1201eb0bf109b6898d07dbc9f8d70bccf61e9bf Mon Sep 17 00:00:00 2001 From: joachim <121795812+faltjo@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:54:02 +0100 Subject: [PATCH 161/490] [5.x] Use deep copy of objects in replicator set (#11621) --- resources/js/components/fieldtypes/replicator/Replicator.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/fieldtypes/replicator/Replicator.vue b/resources/js/components/fieldtypes/replicator/Replicator.vue index 5744aadf503..0826e18f546 100644 --- a/resources/js/components/fieldtypes/replicator/Replicator.vue +++ b/resources/js/components/fieldtypes/replicator/Replicator.vue @@ -205,7 +205,7 @@ export default { addSet(handle, index) { const set = { - ...this.meta.defaults[handle], + ...JSON.parse(JSON.stringify(this.meta.defaults[handle])), _id: uniqid(), type: handle, enabled: true, @@ -228,7 +228,7 @@ export default { const index = this.value.findIndex(v => v._id === old_id); const old = this.value[index]; const set = { - ...old, + ...JSON.parse(JSON.stringify(old)), _id: uniqid(), }; From 49c7a46767d9c3a7d893b7b714c8783cb503104c Mon Sep 17 00:00:00 2001 From: Jack Sleight Date: Wed, 26 Mar 2025 18:33:53 +0000 Subject: [PATCH 162/490] [5.x] Add Edit Blueprint links to create publish forms (#11625) --- resources/js/components/entries/BaseCreateForm.vue | 2 ++ resources/js/components/terms/BaseCreateForm.vue | 2 ++ resources/views/entries/create.blade.php | 2 ++ resources/views/terms/create.blade.php | 2 ++ src/Http/Controllers/CP/Collections/EntriesController.php | 1 + src/Http/Controllers/CP/Taxonomies/TermsController.php | 1 + 6 files changed, 10 insertions(+) diff --git a/resources/js/components/entries/BaseCreateForm.vue b/resources/js/components/entries/BaseCreateForm.vue index 7970232f628..394e701079d 100644 --- a/resources/js/components/entries/BaseCreateForm.vue +++ b/resources/js/components/entries/BaseCreateForm.vue @@ -20,6 +20,7 @@ :breadcrumbs="breadcrumbs" :initial-site="site" :can-manage-publish-state="canManagePublishState" + :can-edit-blueprint="canEditBlueprint" :create-another-url="createAnotherUrl" :initial-listing-url="listingUrl" :preview-targets="previewTargets" @@ -45,6 +46,7 @@ export default { 'breadcrumbs', 'site', 'canManagePublishState', + 'canEditBlueprint', 'createAnotherUrl', 'listingUrl', 'previewTargets', diff --git a/resources/js/components/terms/BaseCreateForm.vue b/resources/js/components/terms/BaseCreateForm.vue index e14a0f44e4d..0ef368f8d98 100644 --- a/resources/js/components/terms/BaseCreateForm.vue +++ b/resources/js/components/terms/BaseCreateForm.vue @@ -17,6 +17,7 @@ :initial-is-root="true" :initial-origin-values="{}" :initial-site="site" + :can-edit-blueprint="canEditBlueprint" :create-another-url="createAnotherUrl" :listing-url="listingUrl" :preview-targets="previewTargets" @@ -39,6 +40,7 @@ export default { 'published', 'localizations', 'site', + 'canEditBlueprint', 'createAnotherUrl', 'listingUrl', 'previewTargets', diff --git a/resources/views/entries/create.blade.php b/resources/views/entries/create.blade.php index 8f7392289e6..79cf4c604c7 100644 --- a/resources/views/entries/create.blade.php +++ b/resources/views/entries/create.blade.php @@ -1,3 +1,4 @@ +@inject('str', 'Statamic\Support\Str') @extends('statamic::layout') @section('title', $breadcrumbs->title($collectionCreateLabel)) @section('wrapper_class', 'max-w-3xl') @@ -19,6 +20,7 @@ site="{{ $locale }}" create-another-url="{{ cp_route('collections.entries.create', [$collection, $locale, 'blueprint' => $blueprint['handle'], 'parent' => $values['parent'] ?? null]) }}" listing-url="{{ cp_route('collections.show', $collection) }}" + :can-edit-blueprint="{{ $str::bool($user->can('configure fields')) }}" :can-manage-publish-state="{{ Statamic\Support\Str::bool($canManagePublishState) }}" :preview-targets="{{ json_encode($previewTargets) }}" > diff --git a/resources/views/terms/create.blade.php b/resources/views/terms/create.blade.php index 4e2d9d5796c..cb0a0917b2c 100644 --- a/resources/views/terms/create.blade.php +++ b/resources/views/terms/create.blade.php @@ -1,3 +1,4 @@ +@inject('str', 'Statamic\Support\Str') @extends('statamic::layout') @section('title', $breadcrumbs->title($taxonomyCreateLabel)) @section('wrapper_class', 'max-w-3xl') @@ -14,6 +15,7 @@ :published="{{ json_encode($published) }}" :localizations="{{ json_encode($localizations) }}" site="{{ $locale }}" + :can-edit-blueprint="{{ $str::bool($user->can('configure fields')) }}" create-another-url="{{ cp_route('taxonomies.terms.create', [$taxonomy, $locale]) }}" listing-url="{{ cp_route('taxonomies.show', $taxonomy) }}" :preview-targets="{{ json_encode($previewTargets) }}" diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 5535e11a303..53180eb1b48 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -321,6 +321,7 @@ public function create(Request $request, $collection, $site) 'title' => $collection->createLabel(), 'actions' => [ 'save' => cp_route('collections.entries.store', [$collection->handle(), $site->handle()]), + 'editBlueprint' => cp_route('collections.blueprints.edit', [$collection, $blueprint]), ], 'values' => $values->all(), 'extraValues' => [ diff --git a/src/Http/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index d336504858c..ee57e9b4c3a 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermsController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermsController.php @@ -243,6 +243,7 @@ public function create(Request $request, $taxonomy, $site) 'title' => $taxonomy->createLabel(), 'actions' => [ 'save' => cp_route('taxonomies.terms.store', [$taxonomy->handle(), $site->handle()]), + 'editBlueprint' => cp_route('taxonomies.blueprints.edit', [$taxonomy, $blueprint]), ], 'values' => $values, 'meta' => $fields->meta(), From 8d477deaea868b7bf3c7ea0dbe95cb8acfa8e842 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 27 Mar 2025 14:23:52 +0000 Subject: [PATCH 163/490] [5.x] Expose field conditions from GraphQL API (#11607) --- src/GraphQL/Types/FieldType.php | 12 ++++++++++++ tests/Feature/GraphQL/FormTest.php | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/Types/FieldType.php b/src/GraphQL/Types/FieldType.php index 76b0d97beba..829bad16c23 100644 --- a/src/GraphQL/Types/FieldType.php +++ b/src/GraphQL/Types/FieldType.php @@ -46,6 +46,18 @@ public function fields(): array return $field->config()['width'] ?? 100; }, ], + 'if' => [ + 'type' => GraphQL::type(ArrayType::NAME), + 'resolve' => function ($field) { + return $field->config()['if'] ?? null; + }, + ], + 'unless' => [ + 'type' => GraphQL::type(ArrayType::NAME), + 'resolve' => function ($field) { + return $field->config()['unless'] ?? null; + }, + ], 'config' => [ 'type' => GraphQL::type(ArrayType::NAME), 'resolve' => function ($field) { diff --git a/tests/Feature/GraphQL/FormTest.php b/tests/Feature/GraphQL/FormTest.php index d5f6ce4178a..73b0b805519 100644 --- a/tests/Feature/GraphQL/FormTest.php +++ b/tests/Feature/GraphQL/FormTest.php @@ -121,8 +121,8 @@ public function it_queries_the_fields() 'invalid' => 'This isnt in the fieldtypes config fields so it shouldnt be output', 'width' => 50, ], - 'subject' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House']], - 'message' => ['type' => 'textarea', 'width' => 33], + 'subject' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House'], 'if' => ['name' => 'not empty']], + 'message' => ['type' => 'textarea', 'width' => 33, 'unless' => ['subject' => 'equals spam']], ]); BlueprintRepository::shouldReceive('find')->with('forms.contact')->andReturn($blueprint); @@ -137,6 +137,8 @@ public function it_queries_the_fields() instructions width config + if + unless } } } @@ -158,6 +160,8 @@ public function it_queries_the_fields() 'config' => [ 'placeholder' => 'Type here...', ], + 'if' => null, + 'unless' => null, ], [ 'handle' => 'subject', @@ -168,6 +172,8 @@ public function it_queries_the_fields() 'config' => [ 'options' => ['disco' => 'Disco', 'house' => 'House'], ], + 'if' => ['name' => 'not empty'], + 'unless' => null, ], [ 'handle' => 'message', @@ -176,6 +182,8 @@ public function it_queries_the_fields() 'instructions' => null, 'width' => 33, 'config' => [], + 'if' => null, + 'unless' => ['subject' => 'equals spam'], ], ], ], From 20c88883ba6a8084b644bd9c013acc4f3720a42a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 27 Mar 2025 17:08:37 +0000 Subject: [PATCH 164/490] [5.x] Fix dates in localizations when duplicating entries (#11361) --- src/Actions/DuplicateEntry.php | 2 +- src/Entries/Entry.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Actions/DuplicateEntry.php b/src/Actions/DuplicateEntry.php index 96e90e24269..f278fd6482a 100644 --- a/src/Actions/DuplicateEntry.php +++ b/src/Actions/DuplicateEntry.php @@ -88,7 +88,7 @@ private function duplicateEntry(Entry $original, ?string $origin = null) $entry->slug($slug); } - if ($original->hasDate()) { + if ($original->hasExplicitDate()) { $entry->date($original->date()); } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 27493c2bd4f..d20c88da10c 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -622,6 +622,11 @@ public function hasSeconds() return $this->blueprint()->field('date')->fieldtype()->secondsEnabled(); } + public function hasExplicitDate(): bool + { + return $this->hasDate() && $this->date; + } + public function sites() { return $this->collection()->sites(); From 07ad39b7e20dbcd6ac2507e6cb1021bc00241695 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 2 Apr 2025 19:09:33 +0200 Subject: [PATCH 165/490] [5.x] Restore error message on asset upload server errors (#11642) Account for empty response on asset uploads Signed-off-by: Philipp Daun --- resources/js/components/assets/Uploader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/assets/Uploader.vue b/resources/js/components/assets/Uploader.vue index e3ffa5013a9..cb704d712e1 100644 --- a/resources/js/components/assets/Uploader.vue +++ b/resources/js/components/assets/Uploader.vue @@ -287,7 +287,7 @@ export default { } } - this.handleToasts(response._toasts ?? []); + this.handleToasts(response?._toasts ?? []); upload.errorMessage = msg; upload.errorStatus = status; From bb58cfd3a9440025bdcceaf8a98898dc59c3704f Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 2 Apr 2025 18:14:14 +0100 Subject: [PATCH 166/490] [5.x] Revert "Escape start_page Preference to avoid invalid Redirect" (#11651) * Revert "[5.x] Escape start_page Preference to avoid invalid Redirect (#11616)" This reverts commit b4b78754a5b1eb73f7eb31dd525427a2476ddcae. * Add test --- .../Controllers/CP/StartPageController.php | 2 +- tests/CP/StartPageTest.php | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/CP/StartPageTest.php diff --git a/src/Http/Controllers/CP/StartPageController.php b/src/Http/Controllers/CP/StartPageController.php index e6ac7ceafa2..918587c30c1 100644 --- a/src/Http/Controllers/CP/StartPageController.php +++ b/src/Http/Controllers/CP/StartPageController.php @@ -10,7 +10,7 @@ public function __invoke() { session()->reflash(); - $url = config('statamic.cp.route').'/'.urlencode(Preference::get('start_page', config('statamic.cp.start_page'))); + $url = config('statamic.cp.route').'/'.Preference::get('start_page', config('statamic.cp.start_page')); return redirect($url); } diff --git a/tests/CP/StartPageTest.php b/tests/CP/StartPageTest.php new file mode 100644 index 00000000000..45d1cee3f88 --- /dev/null +++ b/tests/CP/StartPageTest.php @@ -0,0 +1,46 @@ +setPreference('start_page', 'collections/pages')->save(); + + $this + ->actingAs(User::make()->makeSuper()->save()) + ->get('/cp') + ->assertRedirect('/cp/collections/pages'); + } + + #[Test] + public function it_falls_back_to_start_page_config_option_when_preference_is_missing() + { + config('statamic.cp.start_page', 'dashboard'); + + $this + ->actingAs(User::make()->makeSuper()->save()) + ->get('/cp') + ->assertRedirect('/cp/dashboard'); + } +} From 8ff9324d20811cfb72b4b096f59073d008bc0584 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 9 Apr 2025 19:49:23 +0100 Subject: [PATCH 167/490] [5.x] Fix docblock in AssetContainer facade (#11658) Co-authored-by: Jason Varga --- src/Facades/AssetContainer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/AssetContainer.php b/src/Facades/AssetContainer.php index 1d0644ada07..6347d31e236 100644 --- a/src/Facades/AssetContainer.php +++ b/src/Facades/AssetContainer.php @@ -12,7 +12,7 @@ * @method static \Statamic\Contracts\Assets\AssetContainer findOrFail($id) * @method static \Statamic\Contracts\Assets\AssetContainer make(string $handle = null) * - * @see \Statamic\Assets\AssetRepository + * @see \Statamic\Stache\Repositories\AssetContainerRepository */ class AssetContainer extends Facade { From a39fa2f8bd051cc4c1880ea957957a45fbc6835a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 9 Apr 2025 19:56:06 +0100 Subject: [PATCH 168/490] [5.x] Fix icon selector in nav builder (#11656) Co-authored-by: Jason Varga --- resources/js/components/nav/ItemEditor.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/nav/ItemEditor.vue b/resources/js/components/nav/ItemEditor.vue index 2f69e9ada90..5b5c8ab6051 100644 --- a/resources/js/components/nav/ItemEditor.vue +++ b/resources/js/components/nav/ItemEditor.vue @@ -41,9 +41,9 @@ - + From c1d093236e685910bdcbfcb33bea141ca1f08680 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Wed, 9 Apr 2025 21:05:12 +0200 Subject: [PATCH 169/490] [5.x] Allow dynamic counter names in `increment` tag (#11671) --- src/Tags/Increment.php | 20 ++++++++++++++++---- tests/Tags/IncrementTest.php | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Tags/Increment.php b/src/Tags/Increment.php index 296eae18f17..65414102902 100644 --- a/src/Tags/Increment.php +++ b/src/Tags/Increment.php @@ -36,12 +36,24 @@ public function reset() return ''; } - public function wildcard($tag) + public function index() { - if (! isset(self::$arr[$tag])) { - return self::$arr[$tag] = $this->params->get('from', 0); + $counter = $this->params->get('counter', null); + + return $this->increment($counter); + } + + public function wildcard($counter) + { + return $this->increment($counter); + } + + protected function increment($counter) + { + if (! isset(self::$arr[$counter])) { + return self::$arr[$counter] = $this->params->get('from', 0); } - return self::$arr[$tag] = self::$arr[$tag] + $this->params->get('by', 1); + return self::$arr[$counter] = self::$arr[$counter] + $this->params->get('by', 1); } } diff --git a/tests/Tags/IncrementTest.php b/tests/Tags/IncrementTest.php index 9b8869d09e0..83c643dbf32 100644 --- a/tests/Tags/IncrementTest.php +++ b/tests/Tags/IncrementTest.php @@ -11,12 +11,14 @@ class IncrementTest extends TestCase protected $data = [ 'data' => [ [ + 'category' => 'A', 'articles' => [ ['title' => 'One'], ['title' => 'Two'], ], ], [ + 'category' => 'B', 'articles' => [ ['title' => 'Three'], ['title' => 'Four'], @@ -43,6 +45,19 @@ public function basic_increment_works() ); } + #[Test] + public function increment_with_dynamic_name_works() + { + $template = <<<'EOT' +{{ data }}{{ category }}-{{ articles }}{{ increment :counter="category" by="10" }}-{{ /articles }}{{ /data }} +EOT; + + $this->assertSame( + 'A-0-10-B-0-10-', + $this->tag($template, $this->data) + ); + } + #[Test] public function increment_with_starting_value_works() { From 1251f96ca14d3246a04ffd21138a08ec272b313f Mon Sep 17 00:00:00 2001 From: Frederick Roegiers Date: Thu, 10 Apr 2025 15:44:27 +0200 Subject: [PATCH 170/490] [5.x] Fix typo in nl.json language file (#11686) Fix typo in nl.json language file --- resources/lang/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/nl.json b/resources/lang/nl.json index ddd10c75c7b..e42fff650ab 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -74,7 +74,7 @@ "Allow additions": "Toevoegingen toestaan", "Allow Antlers": "Antlers toestaan", "Allow Any Color": "Alle kleuren toestaan", - "Allow Creating": "Aanmaken toesten", + "Allow Creating": "Aanmaken toestaan", "Allow Downloading": "Downloaden toestaan", "Allow Fullscreen Mode": "Fullscreen-modus toestaan", "Allow Moving": "Verplaatsen toestaan", From 1f4bd767a82835f931d067976f339c2d51da727b Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 10 Apr 2025 10:12:35 -0400 Subject: [PATCH 171/490] changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2814159be88..419f0db0a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Release Notes +## 5.53.0 (2025-04-10) + +### What's new +- Allow dynamic counter names in `increment` tag [#11671](https://github.com/statamic/cms/issues/11671) by @daun +- Expose field conditions from GraphQL API [#11607](https://github.com/statamic/cms/issues/11607) by @duncanmcclean +- Add Edit Blueprint links to create publish forms [#11625](https://github.com/statamic/cms/issues/11625) by @jacksleight + +### What's fixed +- Fix icon selector in nav builder [#11656](https://github.com/statamic/cms/issues/11656) by @duncanmcclean +- Fix docblock in AssetContainer facade [#11658](https://github.com/statamic/cms/issues/11658) by @duncanmcclean +- Revert "Escape start_page Preference to avoid invalid Redirect" [#11651](https://github.com/statamic/cms/issues/11651) by @duncanmcclean +- Restore error message on asset upload server errors [#11642](https://github.com/statamic/cms/issues/11642) by @daun +- Fix dates in localizations when duplicating entries [#11361](https://github.com/statamic/cms/issues/11361) by @duncanmcclean +- Use deep copy of objects in replicator set [#11621](https://github.com/statamic/cms/issues/11621) by @faltjo +- French translations [#11622](https://github.com/statamic/cms/issues/11622) by @ebeauchamps +- Dutch translations [#11686](https://github.com/statamic/cms/issues/11686) by @rogerthat-be + + + ## 5.52.0 (2025-03-25) ### What's new From b6d658def111e2d3a83a1a1483b0d9d5fe017aa8 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 15 Apr 2025 19:08:34 +0100 Subject: [PATCH 172/490] [5.x] Ensure asset references are updated correctly (#11705) --- src/Listeners/UpdateAssetReferences.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Listeners/UpdateAssetReferences.php b/src/Listeners/UpdateAssetReferences.php index a82bf44d562..a7deae9e6fb 100644 --- a/src/Listeners/UpdateAssetReferences.php +++ b/src/Listeners/UpdateAssetReferences.php @@ -85,16 +85,21 @@ protected function replaceReferences($asset, $originalPath, $newPath) $container = $asset->container()->handle(); - $updatedItems = $this + $hasUpdatedItems = false; + + $this ->getItemsContainingData() - ->map(function ($item) use ($container, $originalPath, $newPath) { - return AssetReferenceUpdater::item($item) + ->each(function ($item) use ($container, $originalPath, $newPath, &$hasUpdatedItems) { + $updated = AssetReferenceUpdater::item($item) ->filterByContainer($container) ->updateReferences($originalPath, $newPath); - }) - ->filter(); - if ($updatedItems->isNotEmpty()) { + if ($updated) { + $hasUpdatedItems = true; + } + }); + + if ($hasUpdatedItems) { AssetReferencesUpdated::dispatch($asset); } } From e52783da9d81dbe3a7a3bd3d2e28e41de61fec51 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 15 Apr 2025 19:17:19 +0100 Subject: [PATCH 173/490] [5.x] Remove `templates`/`themes` methods from `CpController` (#11706) --- src/Http/Controllers/CP/CpController.php | 42 ------------------------ 1 file changed, 42 deletions(-) diff --git a/src/Http/Controllers/CP/CpController.php b/src/Http/Controllers/CP/CpController.php index f13f6b770a0..f0058352bb8 100644 --- a/src/Http/Controllers/CP/CpController.php +++ b/src/Http/Controllers/CP/CpController.php @@ -5,13 +5,8 @@ use Illuminate\Auth\Access\AuthorizationException as LaravelAuthException; use Illuminate\Http\Request; use Statamic\Exceptions\AuthorizationException; -use Statamic\Facades\File; -use Statamic\Facades\Folder; -use Statamic\Facades\YAML; use Statamic\Http\Controllers\Controller; use Statamic\Statamic; -use Statamic\Support\Arr; -use Statamic\Support\Str; /** * The base control panel controller. @@ -31,43 +26,6 @@ public function __construct(Request $request) $this->request = $request; } - /** - * Get all the template names from the current theme. - * - * @return array - */ - public function templates() - { - $templates = []; - - foreach (Folder::disk('resources')->getFilesByTypeRecursively('templates', 'html') as $path) { - $parts = explode('/', $path); - array_shift($parts); - $templates[] = Str::removeRight(implode('/', $parts), '.html'); - } - - return $templates; - } - - public function themes() - { - $themes = []; - - foreach (Folder::disk('themes')->getFolders('/') as $folder) { - $name = $folder; - - // Get the name if one exists in a meta file - if (File::disk('themes')->exists($folder.'/meta.yaml')) { - $meta = YAML::parse(File::disk('themes')->get($folder.'/meta.yaml')); - $name = Arr::get($meta, 'name', $folder); - } - - $themes[] = compact('folder', 'name'); - } - - return $themes; - } - /** * 404. */ From df632b6654c42d3052aa7b825ad504e7fc60eddf Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Tue, 15 Apr 2025 20:20:25 +0200 Subject: [PATCH 174/490] [5.x] Handle translation issues in collection widget (#11693) --- resources/views/widgets/collection.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/views/widgets/collection.blade.php b/resources/views/widgets/collection.blade.php index 6da2090228f..78e5b177666 100644 --- a/resources/views/widgets/collection.blade.php +++ b/resources/views/widgets/collection.blade.php @@ -1,4 +1,5 @@ @php use Statamic\Facades\Site; @endphp +@php use function Statamic\trans as __; @endphp
From 4f048d8288802636a297252ea6a4d87a538cad7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Schwei=C3=9Finger?= Date: Thu, 17 Apr 2025 16:11:50 +0200 Subject: [PATCH 175/490] [5.x] Fix collection index search when using a non-dedicated search index (#11711) Co-authored-by: Jason Varga --- src/Http/Controllers/CP/Collections/EntriesController.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 53180eb1b48..bae59f2b432 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -70,7 +70,11 @@ protected function indexQuery($collection) if ($search = request('search')) { if ($collection->hasSearchIndex()) { - return $collection->searchIndex()->ensureExists()->search($search); + return $collection + ->searchIndex() + ->ensureExists() + ->search($search) + ->where('collection', $collection->handle()); } $query->where('title', 'like', '%'.$search.'%'); From 1148a512ee1ae50cc752828bf398f8e838e07c99 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 18 Apr 2025 03:27:31 +1200 Subject: [PATCH 176/490] [5.x] Fix validation of date field nested in a replicator (#11692) Co-authored-by: Jason Varga --- src/Fieldtypes/Date.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Fieldtypes/Date.php b/src/Fieldtypes/Date.php index 481873d0d44..7af05db4457 100644 --- a/src/Fieldtypes/Date.php +++ b/src/Fieldtypes/Date.php @@ -5,6 +5,7 @@ use Carbon\Exceptions\InvalidFormatException; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; use InvalidArgumentException; use Statamic\Facades\GraphQL; use Statamic\Fields\Fieldtype; @@ -371,10 +372,16 @@ public function secondsEnabled() public function preProcessValidatable($value) { - Validator::make( - [$this->field->handle() => $value], - [$this->field->handle() => [new ValidationRule($this)]], - )->validate(); + try { + Validator::make( + ['field' => $value], + ['field' => [new ValidationRule($this)]], + [], + ['field' => $this->field->display()], + )->validate(); + } catch (ValidationException $e) { + throw ValidationException::withMessages([$this->field->fieldPathPrefix() => $e->errors()['field']]); + } if ($value === null) { return null; From 33c07630a07ecabbffed1c87a8dcf11fcef6801f Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 17 Apr 2025 11:32:00 -0400 Subject: [PATCH 177/490] changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 419f0db0a6f..ca1f87cd0fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Release Notes +## 5.53.1 (2025-04-17) + +### What's fixed +- Fix validation of date field nested in a replicator [#11692](https://github.com/statamic/cms/issues/11692) by @liucf +- Fix collection index search when using a non-dedicated search index [#11711](https://github.com/statamic/cms/issues/11711) by @simonerd +- Handle translation issues in collection widget [#11693](https://github.com/statamic/cms/issues/11693) by @daun +- Remove `templates`/`themes` methods from `CpController` [#11706](https://github.com/statamic/cms/issues/11706) by @duncanmcclean +- Ensure asset references are updated correctly [#11705](https://github.com/statamic/cms/issues/11705) by @duncanmcclean + + + ## 5.53.0 (2025-04-10) ### What's new From eecd638ef8857ccc964de90c22f60845e69362cf Mon Sep 17 00:00:00 2001 From: Emin Date: Fri, 18 Apr 2025 21:32:26 +0200 Subject: [PATCH 178/490] [5.x] Make Live Preview force reload JS modules optional (#11715) --- config/live_preview.php | 12 ++++++++++++ src/Http/Responses/DataResponse.php | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/live_preview.php b/config/live_preview.php index 2464d516b2b..0c1b16ed609 100644 --- a/config/live_preview.php +++ b/config/live_preview.php @@ -33,4 +33,16 @@ // ], + /* + |-------------------------------------------------------------------------- + | Force Reload Javascript Modules + |-------------------------------------------------------------------------- + | + | To force a reload, Live Preview appends a timestamp to the URL on + | script tags of type 'module'. You may disable this behavior here. + | + */ + + 'force_reload_js_modules' => true, + ]; diff --git a/src/Http/Responses/DataResponse.php b/src/Http/Responses/DataResponse.php index baca6e78965..0b7129ec390 100644 --- a/src/Http/Responses/DataResponse.php +++ b/src/Http/Responses/DataResponse.php @@ -149,7 +149,7 @@ protected function contents() { $contents = $this->view()->render(); - if ($this->request->isLivePreview()) { + if ($this->request->isLivePreview() && config('statamic.live_preview.force_reload_js_modules', true)) { $contents = $this->versionJavascriptModules($contents); } From 7a775685134caf32f10f697ddf7f5fd1c2a31bac Mon Sep 17 00:00:00 2001 From: Mason Curry Date: Tue, 22 Apr 2025 08:14:10 -0500 Subject: [PATCH 179/490] [5.x] Safer check on parent tag (#11717) Co-authored-by: Jason Varga --- src/Tags/ParentTags.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Tags/ParentTags.php b/src/Tags/ParentTags.php index 6e0586f7516..3d38ce29747 100644 --- a/src/Tags/ParentTags.php +++ b/src/Tags/ParentTags.php @@ -26,7 +26,7 @@ public function wildcard($method) { $var_name = Stringy::removeLeft($this->tag, 'parent:'); - return Arr::get($this->getParent(), $var_name)->value(); + return Arr::get($this->getParent(), $var_name)?->value(); } /** @@ -60,10 +60,8 @@ private function getParentUrl() /** * Get the parent data. - * - * @return string */ - private function getParent() + private function getParent(): ?array { $segments = explode('/', Str::start(Str::after(URL::getCurrent(), Site::current()->url()), '/')); $segment_count = count($segments); From 69e758ed9756f55bceb8ca7cf4dada02c40f1437 Mon Sep 17 00:00:00 2001 From: Mason Curry Date: Tue, 22 Apr 2025 08:53:03 -0500 Subject: [PATCH 180/490] [5.x] Allow custom nocache db connection (#11716) Co-authored-by: Jason Varga --- config/static_caching.php | 2 ++ .../Commands/stubs/statamic_nocache_tables.php.stub | 8 ++++++-- src/StaticCaching/NoCache/DatabaseRegion.php | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/config/static_caching.php b/config/static_caching.php index e190b9729d6..3e4e5fe6e5a 100644 --- a/config/static_caching.php +++ b/config/static_caching.php @@ -125,6 +125,8 @@ 'nocache' => 'cache', + 'nocache_db_connection' => env('STATAMIC_NOCACHE_DB_CONNECTION'), + 'nocache_js_position' => 'body', /* diff --git a/src/Console/Commands/stubs/statamic_nocache_tables.php.stub b/src/Console/Commands/stubs/statamic_nocache_tables.php.stub index dc59f5a6c4e..4d17f65063a 100644 --- a/src/Console/Commands/stubs/statamic_nocache_tables.php.stub +++ b/src/Console/Commands/stubs/statamic_nocache_tables.php.stub @@ -8,7 +8,9 @@ class StatamicNocacheTables extends Migration { public function up() { - Schema::create('NOCACHE_TABLE', function (Blueprint $table) { + Schema::connection( + config('statamic.static_caching.nocache_db_connection') + )->create('NOCACHE_TABLE', function (Blueprint $table) { $table->string('key')->index()->primary(); $table->string('url')->index(); $table->longText('region'); @@ -18,6 +20,8 @@ class StatamicNocacheTables extends Migration public function down() { - Schema::dropIfExists('nocache_regions'); + Schema::connection( + config('statamic.static_caching.nocache_db_connection') + )->dropIfExists('NOCACHE_TABLE'); } } diff --git a/src/StaticCaching/NoCache/DatabaseRegion.php b/src/StaticCaching/NoCache/DatabaseRegion.php index a5f0b50330a..7eac2120873 100644 --- a/src/StaticCaching/NoCache/DatabaseRegion.php +++ b/src/StaticCaching/NoCache/DatabaseRegion.php @@ -15,4 +15,9 @@ class DatabaseRegion extends Model protected $casts = [ 'key' => 'string', ]; + + public function getConnectionName() + { + return config('statamic.static_caching.nocache_db_connection') ?: parent::getConnectionName(); + } } From 7ab6702fba70b76eb4690581d80df9bec546b364 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 22 Apr 2025 14:56:46 +0100 Subject: [PATCH 181/490] [5.x] Fix static caching invalidation for multi-sites (#10669) Co-authored-by: duncanmcclean --- src/StaticCaching/DefaultInvalidator.php | 210 +++++-- src/StaticCaching/Invalidate.php | 22 +- .../StaticCaching/DefaultInvalidatorTest.php | 554 +++++++++++++++++- 3 files changed, 702 insertions(+), 84 deletions(-) diff --git a/src/StaticCaching/DefaultInvalidator.php b/src/StaticCaching/DefaultInvalidator.php index 00ffdce6511..4425a88f3cb 100644 --- a/src/StaticCaching/DefaultInvalidator.php +++ b/src/StaticCaching/DefaultInvalidator.php @@ -6,10 +6,14 @@ use Statamic\Contracts\Entries\Collection; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Forms\Form; -use Statamic\Contracts\Globals\GlobalSet; +use Statamic\Contracts\Globals\Variables; use Statamic\Contracts\Structures\Nav; -use Statamic\Contracts\Taxonomies\Term; +use Statamic\Contracts\Structures\NavTree; +use Statamic\Facades\Site; +use Statamic\Structures\CollectionTree; use Statamic\Support\Arr; +use Statamic\Support\Str; +use Statamic\Taxonomies\LocalizedTerm; class DefaultInvalidator implements Invalidator { @@ -25,19 +29,25 @@ public function __construct(Cacher $cacher, $rules = []) public function invalidate($item) { if ($this->rules === 'all') { - return $this->cacher->flush(); + $this->cacher->flush(); + + return; } if ($item instanceof Entry) { $this->invalidateEntryUrls($item); - } elseif ($item instanceof Term) { + } elseif ($item instanceof LocalizedTerm) { $this->invalidateTermUrls($item); } elseif ($item instanceof Nav) { $this->invalidateNavUrls($item); - } elseif ($item instanceof GlobalSet) { + } elseif ($item instanceof NavTree) { + $this->invalidateNavTreeUrls($item); + } elseif ($item instanceof Variables) { $this->invalidateGlobalUrls($item); } elseif ($item instanceof Collection) { $this->invalidateCollectionUrls($item); + } elseif ($item instanceof CollectionTree) { + $this->invalidateCollectionTreeUrls($item); } elseif ($item instanceof Asset) { $this->invalidateAssetUrls($item); } elseif ($item instanceof Form) { @@ -47,80 +57,182 @@ public function invalidate($item) protected function invalidateFormUrls($form) { - $this->cacher->invalidateUrls( - Arr::get($this->rules, "forms.{$form->handle()}.urls") - ); + $rules = collect(Arr::get($this->rules, "forms.{$form->handle()}.urls")); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = Site::all()->map(function ($site) use ($rules) { + return $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($site->url(), '/').Str::ensureLeft($rule, '/')); + })->flatten()->all(); + + $this->cacher->invalidateUrls([ + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } protected function invalidateAssetUrls($asset) { - $this->cacher->invalidateUrls( - Arr::get($this->rules, "assets.{$asset->container()->handle()}.urls") - ); + $rules = collect(Arr::get($this->rules, "assets.{$asset->container()->handle()}.urls")); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = Site::all()->map(function ($site) use ($rules) { + return $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($site->url(), '/').Str::ensureLeft($rule, '/')); + })->flatten()->all(); + + $this->cacher->invalidateUrls([ + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } protected function invalidateEntryUrls($entry) { - $entry->descendants()->merge([$entry])->each(function ($entry) { - if (! $entry->isRedirect() && $url = $entry->absoluteUrl()) { - $this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url)); - } - }); - - $this->cacher->invalidateUrls( - Arr::get($this->rules, "collections.{$entry->collectionHandle()}.urls") - ); + $rules = collect(Arr::get($this->rules, "collections.{$entry->collectionHandle()}.urls")); + + $urls = $entry->descendants() + ->merge([$entry]) + ->reject(fn ($entry) => $entry->isRedirect()) + ->map->absoluteUrl() + ->all(); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($entry->site()->url(), '/').Str::ensureLeft($rule, '/')) + ->all(); + + $this->cacher->invalidateUrls([ + ...$urls, + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } protected function invalidateTermUrls($term) { - if ($url = $term->absoluteUrl()) { - $this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url)); + $rules = collect(Arr::get($this->rules, "taxonomies.{$term->taxonomyHandle()}.urls")); - $term->taxonomy()->collections()->each(function ($collection) use ($term) { - if ($url = $term->collection($collection)->absoluteUrl()) { - $this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url)); - } - }); + if ($url = $term->absoluteUrl()) { + $urls = $term->taxonomy()->collections() + ->map(fn ($collection) => $term->collection($collection)->absoluteUrl()) + ->filter() + ->prepend($url) + ->all(); } - $this->cacher->invalidateUrls( - Arr::get($this->rules, "taxonomies.{$term->taxonomyHandle()}.urls") - ); + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($term->site()->url(), '/').Str::ensureLeft($rule, '/')) + ->all(); + + $this->cacher->invalidateUrls([ + ...$urls ?? [], + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } protected function invalidateNavUrls($nav) { - $this->cacher->invalidateUrls( - Arr::get($this->rules, "navigation.{$nav->handle()}.urls") - ); + $rules = collect(Arr::get($this->rules, "navigation.{$nav->handle()}.urls")); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = $nav->sites()->map(function ($site) use ($rules) { + return $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight(Site::get($site)->url(), '/').Str::ensureLeft($rule, '/')); + })->flatten()->all(); + + $this->cacher->invalidateUrls([ + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); + } + + protected function invalidateNavTreeUrls($tree) + { + $rules = collect(Arr::get($this->rules, "navigation.{$tree->structure()->handle()}.urls")); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($tree->site()->url(), '/').Str::ensureLeft($rule, '/')) + ->all(); + + $this->cacher->invalidateUrls([ + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } - protected function invalidateGlobalUrls($set) + protected function invalidateGlobalUrls($variables) { - $this->cacher->invalidateUrls( - Arr::get($this->rules, "globals.{$set->handle()}.urls") - ); + $rules = collect(Arr::get($this->rules, "globals.{$variables->globalSet()->handle()}.urls")); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($variables->site()->url(), '/').Str::ensureLeft($rule, '/')) + ->all(); + + $this->cacher->invalidateUrls([ + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } protected function invalidateCollectionUrls($collection) { - if ($url = $collection->absoluteUrl()) { - $this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url)); - } + $rules = collect(Arr::get($this->rules, "collections.{$collection->handle()}.urls")); - $this->cacher->invalidateUrls( - Arr::get($this->rules, "collections.{$collection->handle()}.urls") - ); + $urls = $collection->sites()->map(fn ($site) => $collection->absoluteUrl($site))->filter()->all(); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); + + $prefixedRelativeUrls = $collection->sites()->map(function ($site) use ($rules) { + return $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight(Site::get($site)->url(), '/').Str::ensureLeft($rule, '/')); + })->flatten()->all(); + + $this->cacher->invalidateUrls([ + ...$urls, + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); } - private function splitUrlAndDomain(string $url) + protected function invalidateCollectionTreeUrls($tree) { - $parsed = parse_url($url); + $rules = collect(Arr::get($this->rules, "collections.{$tree->collection()->handle()}.urls")); + + $absoluteUrls = $rules->filter(fn (string $rule) => $this->isAbsoluteUrl($rule))->all(); - return [ - Arr::get($parsed, 'path', '/'), - $parsed['scheme'].'://'.$parsed['host'], - ]; + $prefixedRelativeUrls = $rules + ->reject(fn (string $rule) => $this->isAbsoluteUrl($rule)) + ->map(fn (string $rule) => Str::removeRight($tree->site()->url(), '/').Str::ensureLeft($rule, '/')) + ->all(); + + $this->cacher->invalidateUrls([ + ...$absoluteUrls, + ...$prefixedRelativeUrls, + ]); + } + + private function isAbsoluteUrl(string $url) + { + return isset(parse_url($url)['scheme']); } } diff --git a/src/StaticCaching/Invalidate.php b/src/StaticCaching/Invalidate.php index c12ae792181..b1fc01177e1 100644 --- a/src/StaticCaching/Invalidate.php +++ b/src/StaticCaching/Invalidate.php @@ -14,14 +14,14 @@ use Statamic\Events\EntryScheduleReached; use Statamic\Events\FormDeleted; use Statamic\Events\FormSaved; -use Statamic\Events\GlobalSetDeleted; -use Statamic\Events\GlobalSetSaved; +use Statamic\Events\GlobalVariablesDeleted; +use Statamic\Events\GlobalVariablesSaved; +use Statamic\Events\LocalizedTermDeleted; +use Statamic\Events\LocalizedTermSaved; use Statamic\Events\NavDeleted; use Statamic\Events\NavSaved; use Statamic\Events\NavTreeDeleted; use Statamic\Events\NavTreeSaved; -use Statamic\Events\TermDeleted; -use Statamic\Events\TermSaved; use Statamic\Facades\Form; class Invalidate implements ShouldQueue @@ -34,10 +34,10 @@ class Invalidate implements ShouldQueue EntrySaved::class => 'invalidateEntry', EntryDeleting::class => 'invalidateEntry', EntryScheduleReached::class => 'invalidateEntry', - TermSaved::class => 'invalidateTerm', - TermDeleted::class => 'invalidateTerm', - GlobalSetSaved::class => 'invalidateGlobalSet', - GlobalSetDeleted::class => 'invalidateGlobalSet', + LocalizedTermSaved::class => 'invalidateTerm', + LocalizedTermDeleted::class => 'invalidateTerm', + GlobalVariablesSaved::class => 'invalidateGlobalSet', + GlobalVariablesDeleted::class => 'invalidateGlobalSet', NavSaved::class => 'invalidateNav', NavDeleted::class => 'invalidateNav', FormSaved::class => 'invalidateForm', @@ -79,7 +79,7 @@ public function invalidateTerm($event) public function invalidateGlobalSet($event) { - $this->invalidator->invalidate($event->globals); + $this->invalidator->invalidate($event->variables); } public function invalidateNav($event) @@ -94,12 +94,12 @@ public function invalidateForm($event) public function invalidateCollectionByTree($event) { - $this->invalidator->invalidate($event->tree->collection()); + $this->invalidator->invalidate($event->tree); } public function invalidateNavByTree($event) { - $this->invalidator->invalidate($event->tree->structure()); + $this->invalidator->invalidate($event->tree); } public function invalidateByBlueprint($event) diff --git a/tests/StaticCaching/DefaultInvalidatorTest.php b/tests/StaticCaching/DefaultInvalidatorTest.php index 79571d41b97..5e794fbdd24 100644 --- a/tests/StaticCaching/DefaultInvalidatorTest.php +++ b/tests/StaticCaching/DefaultInvalidatorTest.php @@ -13,16 +13,18 @@ use Statamic\Contracts\Structures\Nav; use Statamic\Contracts\Taxonomies\Taxonomy; use Statamic\Contracts\Taxonomies\Term; +use Statamic\Facades\Site; +use Statamic\Globals\Variables; use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\DefaultInvalidator as Invalidator; +use Statamic\Structures\CollectionTree; +use Statamic\Structures\NavTree; +use Statamic\Structures\Structure; +use Statamic\Taxonomies\LocalizedTerm; +use Tests\TestCase; -class DefaultInvalidatorTest extends \PHPUnit\Framework\TestCase +class DefaultInvalidatorTest extends TestCase { - public function tearDown(): void - { - Mockery::close(); - } - #[Test] public function specifying_all_as_invalidation_rule_will_just_flush_the_cache() { @@ -38,7 +40,52 @@ public function specifying_all_as_invalidation_rule_will_just_flush_the_cache() public function assets_can_trigger_url_invalidation() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrls')->once()->with(['/page/one', '/page/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/page/three', + 'http://localhost/page/one', + 'http://localhost/page/two', + ])->once(); + }); + + $container = tap(Mockery::mock(AssetContainer::class), function ($m) { + $m->shouldReceive('handle')->andReturn('main'); + }); + + $asset = tap(Mockery::mock(Asset::class), function ($m) use ($container) { + $m->shouldReceive('container')->andReturn($container); + }); + + $invalidator = new Invalidator($cacher, [ + 'assets' => [ + 'main' => [ + 'urls' => [ + '/page/one', + '/page/two', + 'http://localhost/page/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($asset)); + } + + #[Test] + public function assets_can_trigger_url_invalidation_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/page/three', + 'http://test.com/page/one', + 'http://test.com/page/two', + 'http://test.fr/page/one', + 'http://test.fr/page/two', + ])->once(); }); $container = tap(Mockery::mock(AssetContainer::class), function ($m) { @@ -55,6 +102,7 @@ public function assets_can_trigger_url_invalidation() 'urls' => [ '/page/one', '/page/two', + 'http://test.com/page/three', ], ], ], @@ -67,13 +115,18 @@ public function assets_can_trigger_url_invalidation() public function collection_urls_can_be_invalidated() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrl')->with('/my/test/collection', 'http://test.com')->once(); - $cacher->shouldReceive('invalidateUrls')->once()->with(['/blog/one', '/blog/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/my/test/collection', + 'http://localhost/blog/three', + 'http://localhost/blog/one', + 'http://localhost/blog/two', + ])->once(); }); $collection = tap(Mockery::mock(Collection::class), function ($m) { - $m->shouldReceive('absoluteUrl')->andReturn('http://test.com/my/test/collection'); $m->shouldReceive('handle')->andReturn('blog'); + $m->shouldReceive('sites')->andReturn(collect(['en'])); + $m->shouldReceive('absoluteUrl')->with('en')->andReturn('http://localhost/my/test/collection'); }); $invalidator = new Invalidator($cacher, [ @@ -82,6 +135,7 @@ public function collection_urls_can_be_invalidated() 'urls' => [ '/blog/one', '/blog/two', + 'http://localhost/blog/three', ], ], ], @@ -90,12 +144,148 @@ public function collection_urls_can_be_invalidated() $this->assertNull($invalidator->invalidate($collection)); } + #[Test] + public function collection_urls_can_be_invalidated_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + 'de' => ['url' => 'http://test.de', 'locale' => 'de_DE'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/my/test/collection', + 'http://test.fr/my/test/collection', + 'http://test.com/blog/three', + 'http://test.com/blog/one', + 'http://test.com/blog/two', + 'http://test.fr/blog/one', + 'http://test.fr/blog/two', + ])->once(); + }); + + $collection = tap(Mockery::mock(Collection::class), function ($m) { + $m->shouldReceive('handle')->andReturn('blog'); + $m->shouldReceive('sites')->andReturn(collect(['en', 'fr'])); + + $m->shouldReceive('absoluteUrl')->with('en')->andReturn('http://test.com/my/test/collection')->once(); + $m->shouldReceive('absoluteUrl')->with('fr')->andReturn('http://test.fr/my/test/collection')->once(); + $m->shouldReceive('absoluteUrl')->with('de')->never(); + }); + + $invalidator = new Invalidator($cacher, [ + 'collections' => [ + 'blog' => [ + 'urls' => [ + '/blog/one', + '/blog/two', + 'http://test.com/blog/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($collection)); + } + + #[Test] + public function collection_urls_can_be_invalidated_by_a_tree() + { + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/blog/three', + 'http://localhost/blog/one', + 'http://localhost/blog/two', + ])->once(); + }); + + $collection = tap(Mockery::mock(Collection::class), function ($m) { + $m->shouldReceive('handle')->andReturn('blog'); + $m->shouldReceive('sites')->andReturn(collect(['en'])); + }); + + $structure = tap(Mockery::mock(Structure::class), function ($m) use ($collection) { + $m->shouldReceive('collection')->andReturn($collection); + }); + + $tree = tap(Mockery::mock(CollectionTree::class), function ($m) use ($collection, $structure) { + $m->shouldReceive('structure')->andReturn($structure); + $m->shouldReceive('collection')->andReturn($collection); + $m->shouldReceive('site')->andReturn(Site::default()); + }); + + $invalidator = new Invalidator($cacher, [ + 'collections' => [ + 'blog' => [ + 'urls' => [ + '/blog/one', + '/blog/two', + 'http://localhost/blog/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($tree)); + } + + #[Test] + public function collection_urls_can_be_invalidated_by_a_tree_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/blog/three', + 'http://test.fr/blog/one', + 'http://test.fr/blog/two', + ])->once(); + }); + + $collection = tap(Mockery::mock(Collection::class), function ($m) { + $m->shouldReceive('handle')->andReturn('blog'); + $m->shouldReceive('sites')->andReturn(collect(['en'])); + }); + + $structure = tap(Mockery::mock(Structure::class), function ($m) use ($collection) { + $m->shouldReceive('collection')->andReturn($collection); + }); + + $tree = tap(Mockery::mock(CollectionTree::class), function ($m) use ($collection, $structure) { + $m->shouldReceive('structure')->andReturn($structure); + $m->shouldReceive('collection')->andReturn($collection); + $m->shouldReceive('site')->andReturn(Site::get('fr')); + }); + + $invalidator = new Invalidator($cacher, [ + 'collections' => [ + 'blog' => [ + 'urls' => [ + '/blog/one', + '/blog/two', + 'http://localhost/blog/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($tree)); + } + #[Test] public function collection_urls_can_be_invalidated_by_an_entry() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrl')->with('/my/test/entry', 'http://test.com')->once(); - $cacher->shouldReceive('invalidateUrls')->once()->with(['/blog/one', '/blog/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/my/test/entry', + 'http://localhost/blog/three', + 'http://localhost/blog/one', + 'http://localhost/blog/two', + ])->once(); }); $entry = tap(Mockery::mock(Entry::class), function ($m) { @@ -103,6 +293,7 @@ public function collection_urls_can_be_invalidated_by_an_entry() $m->shouldReceive('absoluteUrl')->andReturn('http://test.com/my/test/entry'); $m->shouldReceive('collectionHandle')->andReturn('blog'); $m->shouldReceive('descendants')->andReturn(collect()); + $m->shouldReceive('site')->andReturn(Site::default()); }); $invalidator = new Invalidator($cacher, [ @@ -111,6 +302,47 @@ public function collection_urls_can_be_invalidated_by_an_entry() 'urls' => [ '/blog/one', '/blog/two', + 'http://localhost/blog/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($entry)); + } + + #[Test] + public function collection_urls_can_be_invalidated_by_an_entry_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.fr/my/test/entry', + 'http://test.com/blog/three', + 'http://test.fr/blog/one', + 'http://test.fr/blog/two', + ])->once(); + }); + + $entry = tap(Mockery::mock(Entry::class), function ($m) { + $m->shouldReceive('isRedirect')->andReturn(false); + $m->shouldReceive('absoluteUrl')->andReturn('http://test.fr/my/test/entry'); + $m->shouldReceive('collectionHandle')->andReturn('blog'); + $m->shouldReceive('descendants')->andReturn(collect()); + $m->shouldReceive('site')->andReturn(Site::get('fr')); + }); + + $invalidator = new Invalidator($cacher, [ + 'collections' => [ + 'blog' => [ + 'urls' => [ + '/blog/one', + '/blog/two', + 'http://test.com/blog/three', ], ], ], @@ -123,8 +355,10 @@ public function collection_urls_can_be_invalidated_by_an_entry() public function entry_urls_are_not_invalidated_by_an_entry_with_a_redirect() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrl')->never(); - $cacher->shouldReceive('invalidateUrls')->once()->with(['/blog/one', '/blog/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/blog/one', + 'http://localhost/blog/two', + ])->once(); }); $entry = tap(Mockery::mock(Entry::class), function ($m) { @@ -132,6 +366,7 @@ public function entry_urls_are_not_invalidated_by_an_entry_with_a_redirect() $m->shouldReceive('absoluteUrl')->andReturn('http://test.com/my/test/entry'); $m->shouldReceive('collectionHandle')->andReturn('blog'); $m->shouldReceive('descendants')->andReturn(collect()); + $m->shouldReceive('site')->andReturn(Site::default()); }); $invalidator = new Invalidator($cacher, [ @@ -152,9 +387,63 @@ public function entry_urls_are_not_invalidated_by_an_entry_with_a_redirect() public function taxonomy_urls_can_be_invalidated() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrl')->with('/my/test/term', 'http://test.com')->once(); - $cacher->shouldReceive('invalidateUrl')->with('/my/collection/tags/term', 'http://test.com')->once(); - $cacher->shouldReceive('invalidateUrls')->once()->with(['/tags/one', '/tags/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/my/test/term', + 'http://localhost/my/collection/tags/term', + 'http://localhost/tags/three', + 'http://localhost/tags/one', + 'http://localhost/tags/two', + ])->once(); + }); + + $collection = Mockery::mock(Collection::class); + + $taxonomy = tap(Mockery::mock(Taxonomy::class), function ($m) use ($collection) { + $m->shouldReceive('collections')->andReturn(collect([$collection])); + }); + + $term = Mockery::mock(Term::class); + + $localized = tap(Mockery::mock(LocalizedTerm::class), function ($m) use ($term, $taxonomy) { + $m->shouldReceive('term')->andReturn($term); + $m->shouldReceive('taxonomyHandle')->andReturn('tags'); + $m->shouldReceive('taxonomy')->andReturn($taxonomy); + $m->shouldReceive('collection')->andReturn($m); + $m->shouldReceive('site')->andReturn(Site::default()); + $m->shouldReceive('absoluteUrl')->andReturn('http://localhost/my/test/term', 'http://localhost/my/collection/tags/term'); + }); + + $invalidator = new Invalidator($cacher, [ + 'taxonomies' => [ + 'tags' => [ + 'urls' => [ + '/tags/one', + '/tags/two', + 'http://localhost/tags/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($localized)); + } + + #[Test] + public function taxonomy_urls_can_be_invalidated_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.fr/my/test/term', + 'http://test.fr/my/collection/tags/term', + 'http://test.com/tags/three', + 'http://test.fr/tags/one', + 'http://test.fr/tags/two', + ])->once(); }); $collection = Mockery::mock(Collection::class); @@ -163,11 +452,15 @@ public function taxonomy_urls_can_be_invalidated() $m->shouldReceive('collections')->andReturn(collect([$collection])); }); - $term = tap(Mockery::mock(Term::class), function ($m) use ($taxonomy) { - $m->shouldReceive('absoluteUrl')->andReturn('http://test.com/my/test/term', 'http://test.com/my/collection/tags/term'); + $term = Mockery::mock(Term::class); + + $localized = tap(Mockery::mock(LocalizedTerm::class), function ($m) use ($term, $taxonomy) { + $m->shouldReceive('term')->andReturn($term); $m->shouldReceive('taxonomyHandle')->andReturn('tags'); $m->shouldReceive('taxonomy')->andReturn($taxonomy); $m->shouldReceive('collection')->andReturn($m); + $m->shouldReceive('site')->andReturn(Site::get('fr')); + $m->shouldReceive('absoluteUrl')->andReturn('http://test.fr/my/test/term', 'http://test.fr/my/collection/tags/term'); }); $invalidator = new Invalidator($cacher, [ @@ -176,23 +469,29 @@ public function taxonomy_urls_can_be_invalidated() 'urls' => [ '/tags/one', '/tags/two', + 'http://test.com/tags/three', ], ], ], ]); - $this->assertNull($invalidator->invalidate($term)); + $this->assertNull($invalidator->invalidate($localized)); } #[Test] public function navigation_urls_can_be_invalidated() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrls')->once()->with(['/one', '/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/three', + 'http://localhost/one', + 'http://localhost/two', + ])->once(); }); $nav = tap(Mockery::mock(Nav::class), function ($m) { $m->shouldReceive('handle')->andReturn('links'); + $m->shouldReceive('sites')->andReturn(collect(['en'])); }); $invalidator = new Invalidator($cacher, [ @@ -201,6 +500,7 @@ public function navigation_urls_can_be_invalidated() 'urls' => [ '/one', '/two', + 'http://localhost/three', ], ], ], @@ -209,36 +509,241 @@ public function navigation_urls_can_be_invalidated() $this->assertNull($invalidator->invalidate($nav)); } + #[Test] + public function navigation_urls_can_be_invalidated_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + 'de' => ['url' => 'http://test.de', 'locale' => 'de_DE'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/three', + 'http://test.com/one', + 'http://test.com/two', + 'http://test.fr/one', + 'http://test.fr/two', + ])->once(); + }); + + $nav = tap(Mockery::mock(Nav::class), function ($m) { + $m->shouldReceive('handle')->andReturn('links'); + $m->shouldReceive('sites')->andReturn(collect(['en', 'fr'])); + }); + + $invalidator = new Invalidator($cacher, [ + 'navigation' => [ + 'links' => [ + 'urls' => [ + '/one', + '/two', + 'http://test.com/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($nav)); + } + + #[Test] + public function navigation_urls_can_be_invalidated_by_a_tree() + { + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/three', + 'http://localhost/one', + 'http://localhost/two', + ])->once(); + }); + + $nav = tap(Mockery::mock(Nav::class), function ($m) { + $m->shouldReceive('handle')->andReturn('links'); + }); + + $tree = tap(Mockery::mock(NavTree::class), function ($m) use ($nav) { + $m->shouldReceive('structure')->andReturn($nav); + $m->shouldReceive('site')->andReturn(Site::default()); + }); + + $invalidator = new Invalidator($cacher, [ + 'navigation' => [ + 'links' => [ + 'urls' => [ + '/one', + '/two', + 'http://localhost/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($tree)); + } + + #[Test] + public function navigation_urls_can_be_invalidated_by_a_tree_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/three', + 'http://test.fr/one', + 'http://test.fr/two', + ])->once(); + }); + + $nav = tap(Mockery::mock(Nav::class), function ($m) { + $m->shouldReceive('handle')->andReturn('links'); + }); + + $tree = tap(Mockery::mock(NavTree::class), function ($m) use ($nav) { + $m->shouldReceive('structure')->andReturn($nav); + $m->shouldReceive('site')->andReturn(Site::get('fr')); + }); + + $invalidator = new Invalidator($cacher, [ + 'navigation' => [ + 'links' => [ + 'urls' => [ + '/one', + '/two', + 'http://test.com/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($tree)); + } + #[Test] public function globals_urls_can_be_invalidated() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrls')->once()->with(['/one', '/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/three', + 'http://localhost/one', + 'http://localhost/two', + ])->once(); }); $set = tap(Mockery::mock(GlobalSet::class), function ($m) { $m->shouldReceive('handle')->andReturn('social'); }); + $variables = tap(Mockery::mock(Variables::class), function ($m) use ($set) { + $m->shouldReceive('globalSet')->andReturn($set); + $m->shouldReceive('site')->andReturn(Site::default()); + }); + $invalidator = new Invalidator($cacher, [ 'globals' => [ 'social' => [ 'urls' => [ '/one', '/two', + 'http://localhost/three', ], ], ], ]); - $this->assertNull($invalidator->invalidate($set)); + $this->assertNull($invalidator->invalidate($variables)); + } + + #[Test] + public function globals_urls_can_be_invalidated_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/three', + 'http://test.fr/one', + 'http://test.fr/two', + ])->once(); + }); + + $set = tap(Mockery::mock(GlobalSet::class), function ($m) { + $m->shouldReceive('handle')->andReturn('social'); + }); + + $variables = tap(Mockery::mock(Variables::class), function ($m) use ($set) { + $m->shouldReceive('globalSet')->andReturn($set); + $m->shouldReceive('site')->andReturn(Site::get('fr')); + }); + + $invalidator = new Invalidator($cacher, [ + 'globals' => [ + 'social' => [ + 'urls' => [ + '/one', + '/two', + 'http://test.com/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($variables)); } #[Test] public function form_urls_can_be_invalidated() { $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { - $cacher->shouldReceive('invalidateUrls')->once()->with(['/one', '/two']); + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/three', + 'http://localhost/one', + 'http://localhost/two', + ])->once(); + }); + + $form = tap(Mockery::mock(Form::class), function ($m) { + $m->shouldReceive('handle')->andReturn('newsletter'); + }); + + $invalidator = new Invalidator($cacher, [ + 'forms' => [ + 'newsletter' => [ + 'urls' => [ + '/one', + '/two', + 'http://localhost/three', + ], + ], + ], + ]); + + $this->assertNull($invalidator->invalidate($form)); + } + + #[Test] + public function form_urls_can_be_invalidated_in_a_multisite() + { + $this->setSites([ + 'en' => ['url' => 'http://test.com', 'locale' => 'en_US'], + 'fr' => ['url' => 'http://test.fr', 'locale' => 'fr_FR'], + ]); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://test.com/three', + 'http://test.com/one', + 'http://test.com/two', + 'http://test.fr/one', + 'http://test.fr/two', + ])->once(); }); $form = tap(Mockery::mock(Form::class), function ($m) { @@ -251,6 +756,7 @@ public function form_urls_can_be_invalidated() 'urls' => [ '/one', '/two', + 'http://test.com/three', ], ], ], From f9af1f5e4c9adaaf1279287ca4600e893803901f Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 22 Apr 2025 18:11:26 +0100 Subject: [PATCH 182/490] [5.x] Fix dirty state on preferences edit form (#11655) Co-authored-by: Jason Varga --- resources/js/components/preferences/EditForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/preferences/EditForm.vue b/resources/js/components/preferences/EditForm.vue index 3c60acf67e4..109468d050d 100644 --- a/resources/js/components/preferences/EditForm.vue +++ b/resources/js/components/preferences/EditForm.vue @@ -108,7 +108,7 @@ export default { .patch(url, this.currentValues) .then(() => { this.$refs.container.saved(); - location.reload(); + this.$nextTick(() => location.reload()); }) .catch(e => this.handleAxiosError(e)); }, From c3e8b7afca85ff3479450160d67a9d2bc1c84c6f Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 22 Apr 2025 18:15:05 +0100 Subject: [PATCH 183/490] [5.x] Fix appended form config fields when user locale differs from app locale (#11704) --- src/Http/Controllers/CP/Forms/FormsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Forms/FormsController.php b/src/Http/Controllers/CP/Forms/FormsController.php index 5f7e983afcf..384b29f5ad7 100644 --- a/src/Http/Controllers/CP/Forms/FormsController.php +++ b/src/Http/Controllers/CP/Forms/FormsController.php @@ -361,7 +361,7 @@ protected function editFormBlueprint($form) foreach (Form::extraConfigFor($form->handle()) as $handle => $config) { $merged = false; foreach ($fields as $sectionHandle => $section) { - if ($section['display'] == $config['display']) { + if ($section['display'] == __($config['display'])) { $fields[$sectionHandle]['fields'] += $config['fields']; $merged = true; } From 0f067a94183d18ee56a0b53437e7b019596e8f55 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 22 Apr 2025 13:23:02 -0400 Subject: [PATCH 184/490] [5.x] Update caniuse-lite (#11725) --- package-lock.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3096160d3b9..ae975c7526b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4231,9 +4231,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001472", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001472.tgz", - "integrity": "sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg==", + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "dev": true, "funding": [ { @@ -4248,7 +4248,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", From bde2b1423279e8cd9566b9a3b26058c4b089cf50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:32:05 -0400 Subject: [PATCH 185/490] [5.x] Bump @babel/runtime from 7.21.0 to 7.27.0 (#11726) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae975c7526b..da06be6bdcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1697,11 +1697,12 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -8865,9 +8866,10 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regenerator-transform": { "version": "0.15.1", From 4e2ca8257c34280456ac6f134e724783d7747a83 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 23 Apr 2025 07:03:04 +1200 Subject: [PATCH 186/490] [5.x] Fix Add truncate class to flex container in LinkFieldtype (#11689) --- resources/js/components/fieldtypes/LinkFieldtype.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/LinkFieldtype.vue b/resources/js/components/fieldtypes/LinkFieldtype.vue index 16f09af95a4..10371423fde 100644 --- a/resources/js/components/fieldtypes/LinkFieldtype.vue +++ b/resources/js/components/fieldtypes/LinkFieldtype.vue @@ -18,7 +18,7 @@
-
+
From d999a9cdadd8833b7cee2c1fbde091d1529035d4 Mon Sep 17 00:00:00 2001 From: Jeroen Peters Date: Wed, 23 Apr 2025 16:24:27 +0200 Subject: [PATCH 187/490] [5.x] Dutch translations (#11730) Co-authored-by: Jeroen Peters --- resources/lang/nl.json | 16 ++++++++++------ resources/lang/nl/fieldtypes.php | 3 +++ resources/lang/nl/validation.php | 1 + 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/resources/lang/nl.json b/resources/lang/nl.json index e42fff650ab..9fdbd922787 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -198,8 +198,8 @@ "Code Block": "Codeblok", "Collapse": "Ineenstorting", "Collapse All": "Alles inklappen", + "Collapse All Sets": "Alle sets inklappen", "Collapse Set": "Set inklappen", - "Collapse Sets": "Sets inklappen", "Collection": "Collectie", "Collection already exists": "Collectie bestaat al", "Collection created": "Collectie aangemaakt", @@ -395,12 +395,14 @@ "Email Content": "E-mailinhoud", "Email Subject": "E-mailonderwerp", "Emojis": "Emoji's", + "Empty": "Leeg", "Enable Input Rules": "Schakel 'input' regels in", "Enable Line Wrapping": "Line wrapping inschakelen", "Enable Paste Rules": "Schakel 'paste' regels in", "Enable Pro Mode": "Pro Mode activeren", "Enable Publish Dates": "Publiceerdata inschakelen", "Enable Revisions": "Revisies inschakelen", + "Enable Statamic Pro?": "Statamic Pro inschakelen?", "Encryption": "Encryptie", "Enter any internal or external URL.": "Interne of externe URL invoeren.", "Enter URL": "URL invoerern", @@ -429,12 +431,10 @@ "Escape Markup": "Markup escapen", "Everything is up to date.": "Alles is up to date.", "Example": "Voorbeeld", - "Exit Fullscreen Mode": "Volledig scherm modus afsluiten", "Expand": "Uitbreiden", "Expand All": "Alles uitvouwen", + "Expand All Sets": "Alle sets uitvouwen", "Expand Set": "Set uitvouwen", - "Expand Sets": "Sets uitklappen", - "Expand\/Collapse Sets": "Set in-\/uitklappen", "Expect a root page": "Verwacht een root-pagina", "Expired": "Verlopen", "Export Submissions": "Inzendingen exporteren", @@ -622,6 +622,7 @@ "Markdown paths": "Markdown paden", "Markdown theme": "Markdown thema", "Max": "Maximaal", + "Max Columns": "Maximaal aantal kolommen", "Max Depth": "Maximale diepte", "Max Files": "Maximaal aantal bestanden", "Max Items": "Maximaal aantal items", @@ -677,6 +678,7 @@ "No templates to choose from.": "Geen templates om uit te kiezen.", "None": "Geen", "not": "geen", + "Not empty": "Niet leeg", "Not equals": "Niet gelijk aan", "Not Featured": "Niet uitgelicht", "Not listable": "Niet vermeldbaar", @@ -766,6 +768,7 @@ "Released on :date": "Uitgebracht op :date", "Remember me": "Onthoud mij", "Remove": "Verwijder", + "Remove All": "Verwijder alle", "Remove all empty nodes": "Verwijder alle lege nodes", "Remove Asset": "Verwijder asset", "Remove child page|Remove :count child pages": "Verwijder child pagina|Verwijder :count child pagina's", @@ -846,7 +849,6 @@ "Searching in:": "Zoeken in:", "Select": "Selecteer", "Select Across Sites": "Selecteer over sites heen", - "Select asset container": "Selecteer asset-container", "Select Collection(s)": "Selecteer collectie(s)", "Select Dropdown": "Selecteer dropdown", "Select Group": "Groep selecteren", @@ -910,6 +912,7 @@ "Stacked": "Gestapeld", "Start Impersonating": "Start met imiteren", "Start Page": "Start Pagina", + "Start typing to search.": "Start met typen om te zoeken.", "Statamic": "Statamic", "Statamic Pro is required.": "Statamic Pro is vereist.", "Static Page Cache": "Statische paginacache", @@ -949,6 +952,7 @@ "Taxonomy saved": "Taxonomie opgeslagen", "Template": "Template", "Templates": "Templates", + "Term": "Term", "Term created": "Term aangemaakt", "Term deleted": "Term verwijderd", "Term references updated": "Termreferencies geüpdatet", @@ -981,7 +985,6 @@ "Toggle": "Schakelaar", "Toggle Button": "Aan-uit-knop", "Toggle Dark Mode": "Donkere modus aan\/uit", - "Toggle Fullscreen": "Volledig scherm aan\/uit", "Toggle Fullscreen Mode": "Volledige schermodus in\/uitschakelen", "Toggle Header Cell": "Header cel in\/uitschakelen", "Toggle Mobile Nav": "Mobile navigatie aan\/uit", @@ -1010,6 +1013,7 @@ "Uncheck All": "Alles deselecteren", "Underline": "Onderstreept", "Unlink": "Ontkoppelen", + "Unlink All": "Ontkoppel alle", "Unlisted Addons": "Niet-vermelde add-ons", "Unordered List": "Ongeordende lijst", "Unpin from Favorites": "Losmaken van favorieten", diff --git a/resources/lang/nl/fieldtypes.php b/resources/lang/nl/fieldtypes.php index 00ef89e9e25..f496e9d4fed 100644 --- a/resources/lang/nl/fieldtypes.php +++ b/resources/lang/nl/fieldtypes.php @@ -8,6 +8,7 @@ 'array.config.keys' => 'Stel de array keys (variabelen) en optionele labels in.', 'array.config.mode' => 'Dynamische modus geeft de gebruiker controle over de data terwijl keyed modus dat niet doet.', 'array.title' => 'Array', + 'asset_folders.config.container' => 'Kies welke asset-container je voor dit veld wilt gebruiken.', 'assets.config.allow_uploads' => 'Nieuwe file-uploads toestaan', 'assets.config.container' => 'Kies welke asset-container je voor dit veld wilt gebruiken.', 'assets.config.dynamic' => 'Activa worden in een submap geplaatst op basis van de waarde van dit veld.', @@ -168,6 +169,8 @@ 'slug.config.show_regenerate' => 'Toon de "hergenereer" knop om een nieuwe slug te kunnen maken.', 'slug.title' => 'Slug', 'structures.title' => 'Structures', + 'table.config.max_columns' => 'Stel een maximum aantal kolommen in.', + 'table.config.max_rows' => 'Stel een maximum aantal rijen in.', 'table.title' => 'Table', 'taggable.config.options' => 'Zorg voor vooraf gedefinieerde tags die geselecteerd kunnen worden.', 'taggable.config.placeholder' => 'Type en druk op ↩ Enter', diff --git a/resources/lang/nl/validation.php b/resources/lang/nl/validation.php index 4c757a55967..e5b24385597 100644 --- a/resources/lang/nl/validation.php +++ b/resources/lang/nl/validation.php @@ -118,6 +118,7 @@ 'uuid' => 'Moet een geldige UUID zijn.', 'arr_fieldtype' => 'Dit is ongeldig.', 'handle' => 'Mag alleen kleine letters en getallen bevatten met lage strepen als scheidingstekens.', + 'handle_starts_with_number' => 'Mag niet beginnen met een getal.', 'slug' => 'Mag alleen kleine letters en getallen bevatten met lage of gewone strepen als scheidingstekens.', 'code_fieldtype_rulers' => 'Dit is ongeldig.', 'composer_package' => 'Moet een geldige composer package naam zijn (bijv. hasselhoff/kung-fury).', From 573d366fe749ea1f6b847f1aafcbe1883d04e255 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:58:03 +0200 Subject: [PATCH 188/490] [5.x] Update nocache map on response (#11650) Co-authored-by: Jason Varga --- src/StaticCaching/Cachers/FileCacher.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/StaticCaching/Cachers/FileCacher.php b/src/StaticCaching/Cachers/FileCacher.php index 0a0a594b81d..78dc3531d21 100644 --- a/src/StaticCaching/Cachers/FileCacher.php +++ b/src/StaticCaching/Cachers/FileCacher.php @@ -207,13 +207,18 @@ public function getNocacheJs(): string $default = << response.json()) .then((data) => { + map = createMap(); // Recreate map in case the DOM changed. + const regions = data.regions; for (var key in regions) { if (map[key]) map[key].outerHTML = regions[key]; From 139c120db08e99d2e5c0bce00f0de2294217ee6c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 25 Apr 2025 17:15:02 +0100 Subject: [PATCH 189/490] [5.x] Cleanup roles after running `SitesTest@gets_authorized_sites` (#11738) --- tests/Sites/SitesTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Sites/SitesTest.php b/tests/Sites/SitesTest.php index 39897b12396..84a93584790 100644 --- a/tests/Sites/SitesTest.php +++ b/tests/Sites/SitesTest.php @@ -3,6 +3,7 @@ namespace Tests\Sites; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\File; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Role; use Statamic\Facades\User; @@ -35,6 +36,13 @@ public function setUp(): void ]); } + public function tearDown(): void + { + File::delete(resource_path('users/roles.yaml')); + + parent::tearDown(); + } + #[Test] public function gets_all_sites() { From 56f0f31fdb1cb9a277cd943c2eea8c4b3d1e9615 Mon Sep 17 00:00:00 2001 From: Bram de Leeuw Date: Fri, 25 Apr 2025 22:59:42 +0200 Subject: [PATCH 190/490] [5.x] Only apply the published filter when not in preview mode (#11652) Co-authored-by: Jason Varga --- src/GraphQL/Queries/EntryQuery.php | 4 +-- tests/Feature/GraphQL/EntryTest.php | 41 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/Queries/EntryQuery.php b/src/GraphQL/Queries/EntryQuery.php index 9e2e870ff98..bb6f2d0577b 100644 --- a/src/GraphQL/Queries/EntryQuery.php +++ b/src/GraphQL/Queries/EntryQuery.php @@ -69,7 +69,7 @@ public function resolve($root, $args) $query->where('site', $site); } - $filters = $args['filter'] ?? null; + $filters = $args['filter'] ?? []; $this->filterQuery($query, $filters); @@ -107,7 +107,7 @@ public function resolve($root, $args) private function filterQuery($query, $filters) { - if (! isset($filters['status']) && ! isset($filters['published'])) { + if (! request()->isLivePreview() && (! isset($filters['status']) && ! isset($filters['published']))) { $filters['status'] = 'published'; } diff --git a/tests/Feature/GraphQL/EntryTest.php b/tests/Feature/GraphQL/EntryTest.php index c4593d1da15..e54dacdb72d 100644 --- a/tests/Feature/GraphQL/EntryTest.php +++ b/tests/Feature/GraphQL/EntryTest.php @@ -4,6 +4,7 @@ use Facades\Statamic\API\FilterAuthorizer; use Facades\Statamic\API\ResourceAuthorizer; +use Facades\Statamic\CP\LivePreview; use Facades\Statamic\Fields\BlueprintRepository; use Facades\Tests\Factories\EntryFactory; use PHPUnit\Framework\Attributes\DataProvider; @@ -755,4 +756,44 @@ public function it_only_shows_published_entries_by_default() 'title' => 'That will be so rad!', ]]]); } + + #[Test] + public function it_only_shows_unpublished_entries_with_token() + { + FilterAuthorizer::shouldReceive('allowedForSubResources') + ->andReturn(['published', 'status']); + + $entry = EntryFactory::collection('blog') + ->id('6') + ->slug('that-was-so-rad') + ->data(['title' => 'That was so rad!']) + ->published(false) + ->create(); + + LivePreview::tokenize('test-token', $entry); + + $query = <<<'GQL' +{ + entry(id: "6") { + id + title + } +} +GQL; + + $this + ->withoutExceptionHandling() + ->post('/graphql', ['query' => $query]) + ->assertGqlOk() + ->assertExactJson(['data' => ['entry' => null]]); + + $this + ->withoutExceptionHandling() + ->post('/graphql?token=test-token', ['query' => $query]) + ->assertGqlOk() + ->assertExactJson(['data' => ['entry' => [ + 'id' => '6', + 'title' => 'That was so rad!', + ]]]); + } } From 882294e0a4659781cab14e962e69230efd649070 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 28 Apr 2025 14:59:34 +0100 Subject: [PATCH 191/490] [5.x] Add `firstOrfail`, `firstOr`, `sole` and `exists` methods to base query builder (#9976) Co-authored-by: Jason Varga --- src/Query/Builder.php | 48 ++++++++ src/Query/EloquentQueryBuilder.php | 48 ++++++++ .../MultipleRecordsFoundException.php | 9 ++ .../Exceptions/RecordsNotFoundException.php | 9 ++ tests/Data/Entries/EntryQueryBuilderTest.php | 108 ++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 src/Query/Exceptions/MultipleRecordsFoundException.php create mode 100644 src/Query/Exceptions/RecordsNotFoundException.php diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 8098ee97404..e862b428791 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -14,6 +14,8 @@ use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Pattern; use Statamic\Query\Concerns\FakesQueries; +use Statamic\Query\Exceptions\MultipleRecordsFoundException; +use Statamic\Query\Exceptions\RecordsNotFoundException; use Statamic\Query\Scopes\AppliesScopes; abstract class Builder implements Contract @@ -565,6 +567,52 @@ public function first() return $this->get()->first(); } + public function firstOrFail($columns = ['*']) + { + if (! is_null($item = $this->select($columns)->first())) { + return $item; + } + + throw new RecordsNotFoundException(); + } + + public function firstOr($columns = ['*'], ?Closure $callback = null) + { + if ($columns instanceof Closure) { + $callback = $columns; + + $columns = ['*']; + } + + if (! is_null($model = $this->select($columns)->first())) { + return $model; + } + + return $callback(); + } + + public function sole($columns = ['*']) + { + $result = $this->get($columns); + + $count = $result->count(); + + if ($count === 0) { + throw new RecordsNotFoundException(); + } + + if ($count > 1) { + throw new MultipleRecordsFoundException($count); + } + + return $result->first(); + } + + public function exists() + { + return $this->count() >= 1; + } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $page = $page ?: Paginator::resolveCurrentPage($pageName); diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index 7d1ab1f75f8..e95c29c2aca 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -11,6 +11,8 @@ use Statamic\Contracts\Query\Builder; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Blink; +use Statamic\Query\Exceptions\MultipleRecordsFoundException; +use Statamic\Query\Exceptions\RecordsNotFoundException; use Statamic\Query\Scopes\AppliesScopes; use Statamic\Support\Arr; @@ -80,6 +82,52 @@ public function first() return $this->get()->first(); } + public function firstOrFail($columns = ['*']) + { + if (! is_null($item = $this->select($columns)->first($columns))) { + return $item; + } + + throw new RecordsNotFoundException(); + } + + public function firstOr($columns = ['*'], ?Closure $callback = null) + { + if ($columns instanceof Closure) { + $callback = $columns; + + $columns = ['*']; + } + + if (! is_null($model = $this->select($columns)->first())) { + return $model; + } + + return $callback(); + } + + public function sole($columns = ['*']) + { + $result = $this->get($columns); + + $count = $result->count(); + + if ($count === 0) { + throw new RecordsNotFoundException(); + } + + if ($count > 1) { + throw new MultipleRecordsFoundException($count); + } + + return $result->first(); + } + + public function exists() + { + return $this->builder->count() >= 1; + } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $paginator = $this->builder->paginate($perPage, $this->selectableColumns($columns), $pageName, $page); diff --git a/src/Query/Exceptions/MultipleRecordsFoundException.php b/src/Query/Exceptions/MultipleRecordsFoundException.php new file mode 100644 index 00000000000..dfdb6a5cfe6 --- /dev/null +++ b/src/Query/Exceptions/MultipleRecordsFoundException.php @@ -0,0 +1,9 @@ +where('type', 'b')->pluck('slug')->all()); } + + #[Test] + public function entry_can_be_found_using_first_or_fail() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $firstOrFail = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->firstOrFail(); + + $this->assertSame($entry, $firstOrFail); + } + + #[Test] + public function exception_is_thrown_when_entry_does_not_exist_using_first_or_fail() + { + $this->expectException(RecordsNotFoundException::class); + + Entry::query() + ->where('collection', 'posts') + ->where('id', 'ze-hoff') + ->firstOrFail(); + } + + #[Test] + public function entry_can_be_found_using_first_or() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $firstOrFail = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->firstOr(function () { + return 'fallback'; + }); + + $this->assertSame($entry, $firstOrFail); + } + + #[Test] + public function callback_is_called_when_entry_does_not_exist_using_first_or() + { + $firstOrFail = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->firstOr(function () { + return 'fallback'; + }); + + $this->assertSame('fallback', $firstOrFail); + } + + #[Test] + public function sole_entry_is_returned() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $sole = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->sole(); + + $this->assertSame($entry, $sole); + } + + #[Test] + public function exception_is_thrown_by_sole_when_multiple_entries_are_returned_from_query() + { + Collection::make('posts')->save(); + EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + EntryFactory::collection('posts')->id('smoff')->slug('joe-hasselsmoff')->data(['title' => 'Joe Hasselsmoff'])->create(); + + $this->expectException(MultipleRecordsFoundException::class); + + Entry::query() + ->where('collection', 'posts') + ->sole(); + } + + #[Test] + public function exception_is_thrown_by_sole_when_no_entries_are_returned_from_query() + { + $this->expectException(RecordsNotFoundException::class); + + Entry::query() + ->where('collection', 'posts') + ->sole(); + } + + #[Test] + public function exists_returns_true_when_results_are_found() + { + $this->createDummyCollectionAndEntries(); + + $this->assertTrue(Entry::query()->exists()); + } + + #[Test] + public function exists_returns_false_when_no_results_are_found() + { + $this->assertFalse(Entry::query()->exists()); + } } class CustomScope extends Scope From 2b5d4953b9723a9c68ba59a85ff23c426afee97e Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 29 Apr 2025 02:05:11 +1200 Subject: [PATCH 192/490] [5.x] Fix pop-up position of for Bard link inconsistent (#11739) --- resources/js/components/fieldtypes/bard/LinkToolbar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/bard/LinkToolbar.vue b/resources/js/components/fieldtypes/bard/LinkToolbar.vue index cc037df2dee..6d148d47f1d 100644 --- a/resources/js/components/fieldtypes/bard/LinkToolbar.vue +++ b/resources/js/components/fieldtypes/bard/LinkToolbar.vue @@ -57,7 +57,7 @@
From 54de18d6305750a0ceb809818ef1ec884dc4f829 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Mon, 28 Apr 2025 15:12:20 +0100 Subject: [PATCH 193/490] [5.x] GraphQL should return float fieldtype values as floats (#11742) --- src/Fieldtypes/Floatval.php | 6 ++++ .../Fieldtypes/FloatvalFieldtypeTest.php | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/Feature/GraphQL/Fieldtypes/FloatvalFieldtypeTest.php diff --git a/src/Fieldtypes/Floatval.php b/src/Fieldtypes/Floatval.php index 84ba8d877e8..6d92b8e9262 100644 --- a/src/Fieldtypes/Floatval.php +++ b/src/Fieldtypes/Floatval.php @@ -2,6 +2,7 @@ namespace Statamic\Fieldtypes; +use Statamic\Facades\GraphQL; use Statamic\Fields\Fieldtype; use Statamic\Query\Scopes\Filters\Fields\Floatval as FloatFilter; @@ -56,4 +57,9 @@ public function filter() { return new FloatFilter($this); } + + public function toGqlType() + { + return GraphQL::type(GraphQL::float()); + } } diff --git a/tests/Feature/GraphQL/Fieldtypes/FloatvalFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/FloatvalFieldtypeTest.php new file mode 100644 index 00000000000..bb268c14e7e --- /dev/null +++ b/tests/Feature/GraphQL/Fieldtypes/FloatvalFieldtypeTest.php @@ -0,0 +1,30 @@ +createEntryWithFields([ + 'filled' => [ + 'value' => 7.34, + 'field' => ['type' => 'float'], + ], + 'undefined' => [ + 'value' => null, + 'field' => ['type' => 'float'], + ], + ]); + + $this->assertGqlEntryHas('filled, undefined', [ + 'filled' => 7.34, + 'undefined' => null, + ]); + } +} From b439e3534a4d847b7ec8d6939052dd2342096b42 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 29 Apr 2025 04:10:02 +1200 Subject: [PATCH 194/490] [5.x] Fix wrong taxonomies count on multiple sites (#11741) --- .../CP/Taxonomies/TaxonomiesController.php | 2 +- tests/Data/Taxonomies/TaxonomyTest.php | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php b/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php index afd03c44ac1..0901cb18847 100644 --- a/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php +++ b/src/Http/Controllers/CP/Taxonomies/TaxonomiesController.php @@ -34,7 +34,7 @@ public function index() return [ 'id' => $taxonomy->handle(), 'title' => $taxonomy->title(), - 'terms' => $taxonomy->queryTerms()->count(), + 'terms' => $taxonomy->queryTerms()->pluck('slug')->unique()->count(), 'edit_url' => $taxonomy->editUrl(), 'delete_url' => $taxonomy->deleteUrl(), 'terms_url' => cp_route('taxonomies.show', $taxonomy->handle()), diff --git a/tests/Data/Taxonomies/TaxonomyTest.php b/tests/Data/Taxonomies/TaxonomyTest.php index 6d502da2626..05d685bb34f 100644 --- a/tests/Data/Taxonomies/TaxonomyTest.php +++ b/tests/Data/Taxonomies/TaxonomyTest.php @@ -18,7 +18,6 @@ use Statamic\Facades; use Statamic\Facades\Collection; use Statamic\Facades\Entry; -use Statamic\Facades\Site; use Statamic\Facades\User; use Statamic\Fields\Blueprint; use Statamic\Taxonomies\Taxonomy; @@ -291,6 +290,24 @@ public function it_trucates_terms() $this->assertCount(0, $taxonomy->queryTerms()->get()); } + #[Test] + public function it_get_terms_count_from_multi_sites() + { + $this->setSites([ + 'en' => ['url' => '/', 'locale' => 'en_US', 'name' => 'English'], + 'fr' => ['url' => '/', 'locale' => 'fr_FR', 'name' => 'French'], + 'de' => ['url' => '/', 'locale' => 'de_DE', 'name' => 'German'], + ]); + + $taxonomy = tap(Facades\Taxonomy::make('tags')->sites(['en', 'fr', 'de']))->save(); + Facades\Term::make()->taxonomy('tags')->slug('one')->data([])->save(); + Facades\Term::make()->taxonomy('tags')->slug('two')->data([])->save(); + Facades\Term::make()->taxonomy('tags')->slug('three')->data([])->save(); + + $this->assertCount(9, $taxonomy->queryTerms()->get()); + $this->assertEquals(3, $taxonomy->queryTerms()->pluck('slug')->unique()->count()); + } + #[Test] public function it_saves_through_the_api() { From c6fceb4a116ca8ca01784b26db559c14ccbeece1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 30 Apr 2025 00:16:00 +0100 Subject: [PATCH 195/490] [5.x] Hide "Edit" button on relationship fieldtypes when user is missing permissions (#11748) --- .../js/components/inputs/relationship/RelationshipInput.vue | 2 +- src/Fieldtypes/Terms.php | 1 + src/Fieldtypes/Users.php | 1 + src/Http/Resources/CP/Entries/EntriesFieldtypeEntry.php | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/js/components/inputs/relationship/RelationshipInput.vue b/resources/js/components/inputs/relationship/RelationshipInput.vue index 3d890ace8ad..f862a0f5bf8 100644 --- a/resources/js/components/inputs/relationship/RelationshipInput.vue +++ b/resources/js/components/inputs/relationship/RelationshipInput.vue @@ -28,7 +28,7 @@ :item="item" :config="config" :status-icon="statusIcons" - :editable="canEdit" + :editable="canEdit && (item.editable || item.editable === undefined)" :sortable="!readOnly && canReorder" :read-only="readOnly" :form-component="formComponent" diff --git a/src/Fieldtypes/Terms.php b/src/Fieldtypes/Terms.php index ca0268714d2..ed3153b2f13 100644 --- a/src/Fieldtypes/Terms.php +++ b/src/Fieldtypes/Terms.php @@ -373,6 +373,7 @@ protected function toItemArray($id) 'published' => $term->published(), 'private' => $term->private(), 'edit_url' => $term->editUrl(), + 'editable' => User::current()->can('edit', $term), 'hint' => $this->getItemHint($term), ]; } diff --git a/src/Fieldtypes/Users.php b/src/Fieldtypes/Users.php index 641d7d99f5d..86925c6b25d 100644 --- a/src/Fieldtypes/Users.php +++ b/src/Fieldtypes/Users.php @@ -90,6 +90,7 @@ protected function toItemArray($id, $site = null) 'title' => $user->name(), 'id' => $id, 'edit_url' => $user->editUrl(), + 'editable' => User::current()->can('edit', $user), ]; } diff --git a/src/Http/Resources/CP/Entries/EntriesFieldtypeEntry.php b/src/Http/Resources/CP/Entries/EntriesFieldtypeEntry.php index dad09d47d30..2e0fc034205 100644 --- a/src/Http/Resources/CP/Entries/EntriesFieldtypeEntry.php +++ b/src/Http/Resources/CP/Entries/EntriesFieldtypeEntry.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Resources\CP\Entries; use Illuminate\Http\Resources\Json\JsonResource; +use Statamic\Facades\User; use Statamic\Fieldtypes\Entries as EntriesFieldtype; class EntriesFieldtypeEntry extends JsonResource @@ -23,6 +24,7 @@ public function toArray($request) 'title' => $this->resource->value('title'), 'status' => $this->resource->status(), 'edit_url' => $this->resource->editUrl(), + 'editable' => User::current()->can('edit', $this->resource), 'hint' => $this->fieldtype->getItemHint($this->resource), ]; From 05156f224dda349d24633d3eb04c68ca5b11a72e Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 30 Apr 2025 14:16:41 +0100 Subject: [PATCH 196/490] [5.x] Improve error handling in entries fieldtype (#11754) --- src/Fieldtypes/Entries.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index ca794eecb9f..7da9258a681 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -179,7 +179,11 @@ protected function getFirstCollectionFromRequest($request) $collections = $this->getConfiguredCollections(); } - return Collection::findByHandle(Arr::first($collections)); + $collection = Collection::findByHandle($collectionHandle = Arr::first($collections)); + + throw_if(! $collection, new CollectionNotFoundException($collectionHandle)); + + return $collection; } public function getSortColumn($request) From c3751a170a7102fa32da885ed3e4104c8313de3a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 30 Apr 2025 14:17:07 +0100 Subject: [PATCH 197/490] [5.x] Fix ensured author field when sidebar has empty section (#11747) --- src/Fields/Blueprint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php index c44ae5f9fd9..8868a7e0d05 100644 --- a/src/Fields/Blueprint.php +++ b/src/Fields/Blueprint.php @@ -628,7 +628,7 @@ public function removeFieldFromTab($handle, $tab) private function getTabFields($tab) { return collect($this->contents['tabs'][$tab]['sections'])->flatMap(function ($section, $sectionIndex) { - return collect($section['fields'])->map(function ($field, $fieldIndex) use ($sectionIndex) { + return collect($section['fields'] ?? [])->map(function ($field, $fieldIndex) use ($sectionIndex) { return $field + ['fieldIndex' => $fieldIndex, 'sectionIndex' => $sectionIndex]; }); })->keyBy('handle'); From ac50ce121e2db82455f3cc9be381b432a51ad385 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 2 May 2025 15:32:08 -0400 Subject: [PATCH 198/490] changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1f87cd0fc..f43de4e83d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Release Notes +## 5.54.0 (2025-05-02) + +### What's new +- Add `firstOrfail`, `firstOr`, `sole` and `exists` methods to base query builder [#9976](https://github.com/statamic/cms/issues/9976) by @duncanmcclean +- Allow custom nocache db connection [#11716](https://github.com/statamic/cms/issues/11716) by @macaws +- Make Live Preview force reload JS modules optional [#11715](https://github.com/statamic/cms/issues/11715) by @eminos + +### What's fixed +- Fix ensured author field when sidebar has empty section [#11747](https://github.com/statamic/cms/issues/11747) by @duncanmcclean +- Improve error handling in entries fieldtype [#11754](https://github.com/statamic/cms/issues/11754) by @duncanmcclean +- Hide "Edit" button on relationship fieldtypes when user is missing permissions [#11748](https://github.com/statamic/cms/issues/11748) by @duncanmcclean +- Fix wrong taxonomies count on multiple sites [#11741](https://github.com/statamic/cms/issues/11741) by @liucf +- GraphQL should return float fieldtype values as floats [#11742](https://github.com/statamic/cms/issues/11742) by @ryanmitchell +- Fix pop-up position of for Bard link inconsistent [#11739](https://github.com/statamic/cms/issues/11739) by @liucf +- Only apply the published filter when not in preview mode [#11652](https://github.com/statamic/cms/issues/11652) by @TheBnl +- Cleanup roles after running `SitesTest@gets_authorized_sites` [#11738](https://github.com/statamic/cms/issues/11738) by @duncanmcclean +- Update nocache map on response [#11650](https://github.com/statamic/cms/issues/11650) by @indykoning +- Fix Add truncate class to flex container in LinkFieldtype [#11689](https://github.com/statamic/cms/issues/11689) by @liucf +- Fix appended form config fields when user locale differs from app locale [#11704](https://github.com/statamic/cms/issues/11704) by @duncanmcclean +- Fix dirty state on preferences edit form [#11655](https://github.com/statamic/cms/issues/11655) by @duncanmcclean +- Fix static caching invalidation for multi-sites [#10669](https://github.com/statamic/cms/issues/10669) by @duncanmcclean +- Safer check on parent tag [#11717](https://github.com/statamic/cms/issues/11717) by @macaws +- Dutch translations [#11730](https://github.com/statamic/cms/issues/11730) by @jeroenpeters1986 +- Bump @babel/runtime from 7.21.0 to 7.27.0 [#11726](https://github.com/statamic/cms/issues/11726) by @dependabot +- Update caniuse-lite [#11725](https://github.com/statamic/cms/issues/11725) by @jasonvarga + + + ## 5.53.1 (2025-04-17) ### What's fixed From d24cedb4acbd3d5ad087aa4a94c47ba08c967a8f Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 7 May 2025 14:19:51 +0100 Subject: [PATCH 199/490] [5.x] PHPUnit: Use `#[Test]` attribute instead of `/** @test */` (#11767) --- tests/Feature/Forms/UpdateFormTest.php | 2 +- tests/Forms/FormRepositoryTest.php | 2 +- tests/Modifiers/SelectTest.php | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/Feature/Forms/UpdateFormTest.php b/tests/Feature/Forms/UpdateFormTest.php index 65c1717f0ae..8f449c97f6a 100644 --- a/tests/Feature/Forms/UpdateFormTest.php +++ b/tests/Feature/Forms/UpdateFormTest.php @@ -108,7 +108,7 @@ public function it_updates_emails() ], $updated->email()); } - /** @test */ + #[Test] public function it_updates_data() { $form = tap(Form::make('test'))->save(); diff --git a/tests/Forms/FormRepositoryTest.php b/tests/Forms/FormRepositoryTest.php index 3f3011ad0e9..08cca2e1c89 100644 --- a/tests/Forms/FormRepositoryTest.php +++ b/tests/Forms/FormRepositoryTest.php @@ -43,7 +43,7 @@ public function test_find_or_fail_throws_exception_when_form_does_not_exist() $this->repo->findOrFail('does-not-exist'); } - /** @test */ + #[Test] public function it_registers_config() { $this->repo->appendConfigFields('test_form', 'Test Config', [ diff --git a/tests/Modifiers/SelectTest.php b/tests/Modifiers/SelectTest.php index 2a8b5cd58f4..f0a32d7bc16 100644 --- a/tests/Modifiers/SelectTest.php +++ b/tests/Modifiers/SelectTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Mockery; +use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Query\Builder; use Statamic\Entries\EntryCollection; use Statamic\Modifiers\Modify; @@ -12,7 +13,7 @@ class SelectTest extends TestCase { - /** @test */ + #[Test] public function it_selects_certain_values_from_array_of_items() { $items = $this->items(); @@ -38,7 +39,7 @@ public function it_selects_certain_values_from_array_of_items() ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_collections_of_items() { $items = Collection::make($this->items()); @@ -64,7 +65,7 @@ public function it_selects_certain_values_from_collections_of_items() ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_query_builder() { $builder = Mockery::mock(Builder::class); @@ -91,7 +92,7 @@ public function it_selects_certain_values_from_query_builder() ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_array_of_items_with_origins() { $items = $this->itemsWithOrigins(); @@ -121,7 +122,7 @@ public function it_selects_certain_values_from_array_of_items_with_origins() ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_collections_of_items_with_origins() { $items = EntryCollection::make($this->itemsWithOrigins()); @@ -151,7 +152,7 @@ public function it_selects_certain_values_from_collections_of_items_with_origins ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_array_of_items_of_type_array() { $items = $this->itemsOfTypeArray(); @@ -177,7 +178,7 @@ public function it_selects_certain_values_from_array_of_items_of_type_array() ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_collections_of_items_of_type_array() { $items = EntryCollection::make($this->itemsOfTypeArray()); @@ -203,7 +204,7 @@ public function it_selects_certain_values_from_collections_of_items_of_type_arra ); } - /** @test */ + #[Test] public function it_selects_certain_values_from_array_of_items_of_type_arrayaccess() { $items = $this->itemsOfTypeArrayAccess(); From eec1de9ba71229a9e901713e148d788592c9a156 Mon Sep 17 00:00:00 2001 From: joachim <121795812+faltjo@users.noreply.github.com> Date: Wed, 7 May 2025 15:21:34 +0200 Subject: [PATCH 200/490] [5.x] Use deep copy of set's data in bard field (#11766) --- resources/js/components/fieldtypes/bard/BardFieldtype.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/js/components/fieldtypes/bard/BardFieldtype.vue b/resources/js/components/fieldtypes/bard/BardFieldtype.vue index 2e7c1b96dcc..04f94dc5368 100644 --- a/resources/js/components/fieldtypes/bard/BardFieldtype.vue +++ b/resources/js/components/fieldtypes/bard/BardFieldtype.vue @@ -457,8 +457,8 @@ export default { methods: { addSet(handle) { const id = uniqid(); - const values = Object.assign({}, { type: handle }, this.meta.defaults[handle]); - + const deepCopy = JSON.parse(JSON.stringify(this.meta.defaults[handle])); + const values = Object.assign({}, { type: handle }, deepCopy); let previews = {}; Object.keys(this.meta.defaults[handle]).forEach(key => previews[key] = null); this.previews = Object.assign({}, this.previews, { [id]: previews }); @@ -481,7 +481,8 @@ export default { duplicateSet(old_id, attrs, pos) { const id = uniqid(); const enabled = attrs.enabled; - const values = Object.assign({}, attrs.values); + const deepCopy = JSON.parse(JSON.stringify(attrs.values)); + const values = Object.assign({}, deepCopy); let previews = Object.assign({}, this.previews[old_id]); this.previews = Object.assign({}, this.previews, { [id]: previews }); From f2becd137adffdc64c648856ebc912931e5c150f Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Wed, 7 May 2025 06:22:14 -0700 Subject: [PATCH 201/490] [5.x] Ability to select entries from all sites from Bard link (#11768) --- resources/js/components/fieldtypes/bard/LinkToolbar.vue | 1 + resources/lang/en/fieldtypes.php | 1 + src/Fieldtypes/Bard.php | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/resources/js/components/fieldtypes/bard/LinkToolbar.vue b/resources/js/components/fieldtypes/bard/LinkToolbar.vue index 6d148d47f1d..5337dad8750 100644 --- a/resources/js/components/fieldtypes/bard/LinkToolbar.vue +++ b/resources/js/components/fieldtypes/bard/LinkToolbar.vue @@ -265,6 +265,7 @@ export default { type: 'entries', collections: this.collections, max_items: 1, + select_across_sites: this.config.select_across_sites, }; }, diff --git a/resources/lang/en/fieldtypes.php b/resources/lang/en/fieldtypes.php index f493e2394d8..8e24f7e3bb5 100644 --- a/resources/lang/en/fieldtypes.php +++ b/resources/lang/en/fieldtypes.php @@ -44,6 +44,7 @@ 'bard.config.section.editor.instructions' => 'Configure the editor\'s appearance and general behavior.', 'bard.config.section.links.instructions' => 'Configure how links are handled in this instance of Bard.', 'bard.config.section.sets.instructions' => 'Configure blocks of fields that can be inserted anywhere in your Bard content.', + 'bard.config.select_across_sites' => 'Allow selecting entries from other sites. This also disables localizing options on the front-end. Learn more in the [documentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', 'bard.config.smart_typography' => 'Convert common text patterns with the proper typographic characters.', 'bard.config.target_blank' => 'Set `target="_blank"` on all links.', 'bard.config.toolbar_mode' => '**Fixed** mode will keep the toolbar visible at all times, while **floating** only appears while selecting text.', diff --git a/src/Fieldtypes/Bard.php b/src/Fieldtypes/Bard.php index d92c3bec0bc..c7466482cfc 100644 --- a/src/Fieldtypes/Bard.php +++ b/src/Fieldtypes/Bard.php @@ -182,6 +182,11 @@ protected function configFieldItems(): array 'type' => 'collections', 'mode' => 'select', ], + 'select_across_sites' => [ + 'display' => __('Select Across Sites'), + 'instructions' => __('statamic::fieldtypes.bard.config.select_across_sites'), + 'type' => 'toggle', + ], 'container' => [ 'display' => __('Container'), 'instructions' => __('statamic::fieldtypes.bard.config.container'), From d3f4f705fc6ffc6a77597943877bde87367849f1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 8 May 2025 17:23:10 +0100 Subject: [PATCH 202/490] [5.x] Ensure `null` values are filtered out in dictionary field config (#11773) --- src/Fieldtypes/DictionaryFields.php | 2 +- tests/Fieldtypes/DictionaryFieldsTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Fieldtypes/DictionaryFields.php b/src/Fieldtypes/DictionaryFields.php index 89eda653d01..54515d039e2 100644 --- a/src/Fieldtypes/DictionaryFields.php +++ b/src/Fieldtypes/DictionaryFields.php @@ -71,7 +71,7 @@ public function process($data): string|array return $dictionary->handle(); } - return array_merge(['type' => $dictionary->handle()], $values->all()); + return array_merge(['type' => $dictionary->handle()], $values->filter()->all()); } public function extraRules(): array diff --git a/tests/Fieldtypes/DictionaryFieldsTest.php b/tests/Fieldtypes/DictionaryFieldsTest.php index f2b71041f29..11e5a103aa5 100644 --- a/tests/Fieldtypes/DictionaryFieldsTest.php +++ b/tests/Fieldtypes/DictionaryFieldsTest.php @@ -116,6 +116,23 @@ public function it_processes_dictionary_fields_into_a_string_when_dictionary_has $this->assertEquals('fake_dictionary', $process); } + #[Test] + public function it_processes_dictionary_fields_and_filters_out_null_values() + { + $fieldtype = FieldtypeRepository::find('dictionary_fields'); + + $process = $fieldtype->process([ + 'type' => 'fake_dictionary', + 'category' => 'foo', + 'foo' => null, + ]); + + $this->assertEquals([ + 'type' => 'fake_dictionary', + 'category' => 'foo', + ], $process); + } + #[Test] public function it_returns_validation_rules() { From 31c424685548801b51b628b0e42f6fc603f75145 Mon Sep 17 00:00:00 2001 From: Rinus van Dam <6650702+rinusvandam@users.noreply.github.com> Date: Mon, 12 May 2025 22:27:44 +0200 Subject: [PATCH 203/490] [5.x] Dutch translations (#11783) --- resources/lang/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 9fdbd922787..ae861d2d62a 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -506,7 +506,7 @@ "Heading 1": "Kop 1", "Heading 2": "Kop 2", "Heading 3": "Kop 3", - "Heading 4": "Kop 3", + "Heading 4": "Kop 4", "Heading 5": "Kop 5", "Heading 6": "Kop 6", "Heading Anchors": "Kop anchors", From dc53707ab969e834d56b786f22e7317c4f7479fa Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 13 May 2025 20:32:42 +0100 Subject: [PATCH 204/490] [5.x] Fix creating terms in non-default sites (#11746) --- src/Http/Controllers/CP/Taxonomies/TermsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index ee57e9b4c3a..e6a5814492f 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermsController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermsController.php @@ -296,7 +296,7 @@ public function store(Request $request, $taxonomy, $site) $slug = $request->slug; $published = $request->get('published'); // TODO - $defaultSite = Site::default()->handle(); + $defaultSite = $term->taxonomy()->sites()->first(); // If the term is *not* being created in the default site, we'll copy all the // appropriate values into the default localization since it needs to exist. From b60407e2a17dae5533bff847d04ea3da69180e4e Mon Sep 17 00:00:00 2001 From: Rowdy Klijnsmit Date: Tue, 13 May 2025 21:43:05 +0200 Subject: [PATCH 205/490] [5.x] Allow selecting entries from different sites within the link fieldtype (#10546) Co-authored-by: edalzell --- src/Fieldtypes/Link.php | 40 +++++++++++++++++++++++++-- src/Fieldtypes/Link/ArrayableLink.php | 11 +++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/Fieldtypes/Link.php b/src/Fieldtypes/Link.php index 61a9371e567..e932ad5f4cf 100644 --- a/src/Fieldtypes/Link.php +++ b/src/Fieldtypes/Link.php @@ -37,6 +37,11 @@ protected function configFieldItems(): array 'mode' => 'select', 'max_items' => 1, ], + 'select_across_sites' => [ + 'display' => __('Select Across Sites'), + 'instructions' => __('statamic::fieldtypes.entries.config.select_across_sites'), + 'type' => 'toggle', + ], ], ], ]; @@ -44,10 +49,13 @@ protected function configFieldItems(): array public function augment($value) { + $localize = ! $this->canSelectAcrossSites(); + return new ArrayableLink( $value - ? ResolveRedirect::item($value, $this->field->parent(), true) - : null + ? ResolveRedirect::item($value, $this->field->parent(), $localize) + : null, + ['select_across_sites' => $this->canSelectAcrossSites()] ); } @@ -108,6 +116,7 @@ private function nestedEntriesFieldtype($value): Fieldtype 'type' => 'entries', 'max_items' => 1, 'create' => false, + 'select_across_sites' => $this->canSelectAcrossSites(), ])); $entryField->setValue($value); @@ -190,4 +199,31 @@ public function toGqlType() }, ]; } + + protected function getConfiguredCollections() + { + return empty($collections = $this->config('collections')) + ? \Statamic\Facades\Collection::handles()->all() + : $collections; + } + + private function canSelectAcrossSites(): bool + { + return $this->config('select_across_sites', false); + } + + private function availableSites() + { + if (! Site::hasMultiple()) { + return []; + } + + $configuredSites = collect($this->getConfiguredCollections())->flatMap(fn ($collection) => \Statamic\Facades\Collection::find($collection)->sites()); + + return Site::authorized() + ->when(isset($configuredSites), fn ($sites) => $sites->filter(fn ($site) => $configuredSites->contains($site->handle()))) + ->map->handle() + ->values() + ->all(); + } } diff --git a/src/Fieldtypes/Link/ArrayableLink.php b/src/Fieldtypes/Link/ArrayableLink.php index 34658795f42..25e9a1458c8 100644 --- a/src/Fieldtypes/Link/ArrayableLink.php +++ b/src/Fieldtypes/Link/ArrayableLink.php @@ -2,6 +2,7 @@ namespace Statamic\Fieldtypes\Link; +use Illuminate\Support\Arr; use Statamic\Fields\ArrayableString; class ArrayableLink extends ArrayableString @@ -26,6 +27,14 @@ public function jsonSerialize() public function url() { - return is_object($this->value) ? $this->value?->url() : $this->value; + if (! is_object($this->value)) { + return $this->value; + } + + if (Arr::get($this->extra(), 'select_across_sites')) { + return $this->value->absoluteUrl(); + } + + return $this->value?->url(); } } From ab715fdf72b63f848f4090f7f056667ade9d77d5 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 13 May 2025 20:46:56 +0100 Subject: [PATCH 206/490] [5.x] Update GraphiQL (#11780) --- resources/views/graphql/graphiql.blade.php | 124 ++++++++++++++------- 1 file changed, 84 insertions(+), 40 deletions(-) diff --git a/resources/views/graphql/graphiql.blade.php b/resources/views/graphql/graphiql.blade.php index 6813cdcd425..ac657ef4301 100644 --- a/resources/views/graphql/graphiql.blade.php +++ b/resources/views/graphql/graphiql.blade.php @@ -1,53 +1,97 @@ - - + + + + GraphiQL ‹ Statamic - - - - - - - - -
Loading...
- + + + +
+
Loading…
+
- + \ No newline at end of file From 506f5e103460ee3bd93609f3b27cfd54628fc008 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 13 May 2025 20:50:14 +0100 Subject: [PATCH 207/490] [5.x] Add `increment`/`decrement` methods to `ContainsData` (#11786) --- src/Data/ContainsData.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Data/ContainsData.php b/src/Data/ContainsData.php index f96928e4e41..be9ea619a8a 100644 --- a/src/Data/ContainsData.php +++ b/src/Data/ContainsData.php @@ -25,6 +25,18 @@ public function set($key, $value) return $this; } + public function increment($key, $amount = 1) + { + $this->data[$key] = ($this->data[$key] ?? 0) + $amount; + + return $this; + } + + public function decrement($key, $amount = 1) + { + return $this->increment($key, -$amount); + } + public function remove($key) { unset($this->data[$key]); From 78b37a4fcd8b830b5a8784d04e682e1a9d4e3657 Mon Sep 17 00:00:00 2001 From: Jack Sleight Date: Tue, 13 May 2025 21:02:24 +0100 Subject: [PATCH 208/490] [5.x] Clone internal data collections (#11777) --- src/Assets/Asset.php | 6 ++++++ src/Auth/File/User.php | 6 ++++++ src/Auth/UserGroup.php | 8 ++++++++ src/Entries/Entry.php | 6 ++++++ src/Forms/Form.php | 7 +++++++ src/Forms/Submission.php | 6 ++++++ src/Globals/Variables.php | 6 ++++++ src/Taxonomies/LocalizedTerm.php | 6 ++++++ src/Taxonomies/Term.php | 8 ++++++++ tests/Assets/AssetTest.php | 18 ++++++++++++++++++ tests/Auth/FileUserTest.php | 18 ++++++++++++++++++ tests/Auth/UserGroupTest.php | 18 ++++++++++++++++++ tests/Data/Entries/EntryTest.php | 18 ++++++++++++++++++ tests/Data/Globals/VariablesTest.php | 19 +++++++++++++++++++ tests/Data/Taxonomies/TermTest.php | 20 ++++++++++++++++++++ tests/Forms/FormTest.php | 18 ++++++++++++++++++ tests/Forms/SubmissionTest.php | 20 ++++++++++++++++++++ 17 files changed, 208 insertions(+) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index 30ed42b9e8d..d0d994d8c89 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -116,6 +116,12 @@ public function __construct() $this->supplements = collect(); } + public function __clone() + { + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; + } + public function id($id = null) { if ($id) { diff --git a/src/Auth/File/User.php b/src/Auth/File/User.php index 65c305b1775..b848fb21371 100644 --- a/src/Auth/File/User.php +++ b/src/Auth/File/User.php @@ -39,6 +39,12 @@ public function __construct() $this->supplements = collect(); } + public function __clone() + { + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; + } + public function data($data = null) { if (func_num_args() === 0) { diff --git a/src/Auth/UserGroup.php b/src/Auth/UserGroup.php index f24ed4b36f4..75cc314fabe 100644 --- a/src/Auth/UserGroup.php +++ b/src/Auth/UserGroup.php @@ -27,6 +27,14 @@ public function __construct() { $this->roles = collect(); $this->data = collect(); + $this->supplements = collect(); + } + + public function __clone() + { + $this->roles = clone $this->roles; + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; } public function title(?string $title = null) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index d20c88da10c..e83ed2d3b6f 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -88,6 +88,12 @@ public function __construct() $this->supplements = collect(); } + public function __clone() + { + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; + } + public function id($id = null) { return $this->fluentlyGetOrSet('id')->args(func_get_args()); diff --git a/src/Forms/Form.php b/src/Forms/Form.php index e5af45130d7..d093537ead2 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -45,6 +45,13 @@ class Form implements Arrayable, Augmentable, FormContract public function __construct() { $this->data = collect(); + $this->supplements = collect(); + } + + public function __clone() + { + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; } /** diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index 1442ee84fa5..b623de4b9cb 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -48,6 +48,12 @@ public function __construct() $this->supplements = collect(); } + public function __clone() + { + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; + } + /** * Get or set the ID. * diff --git a/src/Globals/Variables.php b/src/Globals/Variables.php index fb7cec42f8f..8d7992f4467 100644 --- a/src/Globals/Variables.php +++ b/src/Globals/Variables.php @@ -44,6 +44,12 @@ public function __construct() $this->supplements = collect(); } + public function __clone() + { + $this->data = clone $this->data; + $this->supplements = clone $this->supplements; + } + public function globalSet($set = null) { return $this->fluentlyGetOrSet('set') diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php index 41d49858975..b6eb3137320 100644 --- a/src/Taxonomies/LocalizedTerm.php +++ b/src/Taxonomies/LocalizedTerm.php @@ -52,6 +52,12 @@ public function __construct($term, $locale) $this->supplements = collect(); } + public function __clone() + { + $this->term = clone $this->term; + $this->supplements = clone $this->supplements; + } + public function get($key, $fallback = null) { return $this->data()->get($key, $fallback); diff --git a/src/Taxonomies/Term.php b/src/Taxonomies/Term.php index 667b9f466e9..45438958ac1 100644 --- a/src/Taxonomies/Term.php +++ b/src/Taxonomies/Term.php @@ -37,6 +37,14 @@ public function __construct() $this->data = collect(); } + public function __clone() + { + $this->data = clone $this->data; + $this->data->transform(function ($data) { + return clone $data; + }); + } + public function id() { return $this->taxonomyHandle().'::'.$this->slug(); diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index ccfa5ed0478..8fc4992adf1 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -2657,4 +2657,22 @@ public function it_uses_a_custom_cache_store() // ideally we would have checked the store name, but laravel 10 doesnt give us a way to do that $this->assertStringContainsString('asset-meta', $store->getStore()->getDirectory()); } + + #[Test] + public function it_clones_internal_collections() + { + $asset = (new Asset)->container($this->container)->path('foo/test.txt'); + $asset->set('foo', 'A'); + $asset->setSupplement('bar', 'A'); + + $clone = clone $asset; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $asset->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $asset->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Auth/FileUserTest.php b/tests/Auth/FileUserTest.php index 6ef2f752444..c0b95084c02 100644 --- a/tests/Auth/FileUserTest.php +++ b/tests/Auth/FileUserTest.php @@ -168,4 +168,22 @@ public function it_prevents_saving_duplicate_groups() $this->assertEquals(['a', 'b', 'c'], $user->get('groups')); } + + #[Test] + public function it_clones_internal_collections() + { + $user = $this->user(); + $user->set('foo', 'A'); + $user->setSupplement('bar', 'A'); + + $clone = clone $user; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $user->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $user->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Auth/UserGroupTest.php b/tests/Auth/UserGroupTest.php index 99f76bff8a2..2d48b1bce1f 100644 --- a/tests/Auth/UserGroupTest.php +++ b/tests/Auth/UserGroupTest.php @@ -374,4 +374,22 @@ public function it_gets_blueprint_values() $this->assertEquals($group->get('one'), $data['one']); $this->assertEquals($group->get('two'), $data['two']); } + + #[Test] + public function it_clones_internal_collections() + { + $group = UserGroup::make(); + $group->set('foo', 'A'); + $group->setSupplement('bar', 'A'); + + $clone = clone $group; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $group->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $group->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index ca48dd407db..d5f788782db 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -2597,4 +2597,22 @@ public function initially_saved_entry_gets_put_into_events() ['7', '7'], ], $events->map(fn ($event) => [$event->entry->id(), $event->initiator->id()])->all()); } + + #[Test] + public function it_clones_internal_collections() + { + $entry = EntryFactory::collection('test')->create(); + $entry->set('foo', 'A'); + $entry->setSupplement('bar', 'A'); + + $clone = clone $entry; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $entry->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $entry->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Data/Globals/VariablesTest.php b/tests/Data/Globals/VariablesTest.php index a5dd9ec6892..5066b31ef0b 100644 --- a/tests/Data/Globals/VariablesTest.php +++ b/tests/Data/Globals/VariablesTest.php @@ -387,4 +387,23 @@ public function augment($values) 'charlie' => ['augmented c', 'augmented d'], ], Arr::only($variables->selectedQueryRelations(['charlie'])->toArray(), ['alfa', 'bravo', 'charlie'])); } + + #[Test] + public function it_clones_internal_collections() + { + $global = GlobalSet::make('test'); + $variables = $global->makeLocalization('en'); + $variables->set('foo', 'A'); + $variables->setSupplement('bar', 'A'); + + $clone = clone $variables; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $variables->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $variables->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Data/Taxonomies/TermTest.php b/tests/Data/Taxonomies/TermTest.php index b2352c3bb7e..4397d6daa9e 100644 --- a/tests/Data/Taxonomies/TermTest.php +++ b/tests/Data/Taxonomies/TermTest.php @@ -481,4 +481,24 @@ public function it_deletes_quietly() $this->assertTrue($return); } + + #[Test] + public function it_clones_internal_collections() + { + $taxonomy = (new TaxonomiesTaxonomy)->handle('tags')->save(); + $term = (new Term)->taxonomy('tags')->slug('foo')->data(['foo' => 'bar'])->inDefaultLocale(); + + $term->set('foo', 'A'); + $term->setSupplement('bar', 'A'); + + $clone = clone $term; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $term->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $term->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Forms/FormTest.php b/tests/Forms/FormTest.php index 46cc05daa98..8401c3adb63 100644 --- a/tests/Forms/FormTest.php +++ b/tests/Forms/FormTest.php @@ -250,4 +250,22 @@ public function it_deletes_quietly() $this->assertTrue($return); } + + #[Test] + public function it_clones_internal_collections() + { + $form = Form::make('contact_us'); + $form->set('foo', 'A'); + $form->setSupplement('bar', 'A'); + + $clone = clone $form; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $form->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $form->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } diff --git a/tests/Forms/SubmissionTest.php b/tests/Forms/SubmissionTest.php index c9d255ad3a9..e631fcbe35e 100644 --- a/tests/Forms/SubmissionTest.php +++ b/tests/Forms/SubmissionTest.php @@ -208,4 +208,24 @@ public function it_deletes_quietly() $this->assertTrue($return); } + + #[Test] + public function it_clones_internal_collections() + { + $form = Form::make('contact_us'); + $form->save(); + $submission = $form->makeSubmission(); + $submission->set('foo', 'A'); + $submission->setSupplement('bar', 'A'); + + $clone = clone $submission; + $clone->set('foo', 'B'); + $clone->setSupplement('bar', 'B'); + + $this->assertEquals('A', $submission->get('foo')); + $this->assertEquals('B', $clone->get('foo')); + + $this->assertEquals('A', $submission->getSupplement('bar')); + $this->assertEquals('B', $clone->getSupplement('bar')); + } } From de4b0cbc3170d8e1c98dd6994fde3a87474db322 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Wed, 14 May 2025 15:28:09 +0200 Subject: [PATCH 209/490] [5.x] Middleware to redirect absolute domains ending in dot (#11782) Co-authored-by: Jason Varga --- .../Middleware/RedirectAbsoluteDomains.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Http/Middleware/RedirectAbsoluteDomains.php diff --git a/src/Http/Middleware/RedirectAbsoluteDomains.php b/src/Http/Middleware/RedirectAbsoluteDomains.php new file mode 100644 index 00000000000..bd93b15eee0 --- /dev/null +++ b/src/Http/Middleware/RedirectAbsoluteDomains.php @@ -0,0 +1,27 @@ +getHost(); + + if (! Str::endsWith($host, '.')) { + return $next($request); + } + + return redirect()->to(Str::replaceFirst($host, rtrim($host, '.'), $request->fullUrl()), 308); + } +} From ef8514d6e353c20e16891a733825b67d32e03578 Mon Sep 17 00:00:00 2001 From: Jack Sleight Date: Wed, 14 May 2025 14:28:28 +0100 Subject: [PATCH 210/490] [5.x] Fix filtering group fieldtype null values (#11788) --- src/Fieldtypes/Group.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fieldtypes/Group.php b/src/Fieldtypes/Group.php index d9807da6b39..9b70ebea085 100644 --- a/src/Fieldtypes/Group.php +++ b/src/Fieldtypes/Group.php @@ -50,7 +50,7 @@ protected function configFieldItems(): array public function process($data) { - return $this->fields()->addValues($data ?? [])->process()->values()->all(); + return $this->fields()->addValues($data ?? [])->process()->values()->filter()->all(); } public function preProcess($data) From ac1ab0c900b3ecbcb0696e3e330729be285eb8f8 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 15 May 2025 03:46:26 +1200 Subject: [PATCH 211/490] [5.x] Fix Term filter on entry listing not working when limiting to 1 term (#11735) Co-authored-by: Duncan McClean --- src/Query/Scopes/Filters/Fields/Terms.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Query/Scopes/Filters/Fields/Terms.php b/src/Query/Scopes/Filters/Fields/Terms.php index 9b96f2ecc14..b7f63b71384 100644 --- a/src/Query/Scopes/Filters/Fields/Terms.php +++ b/src/Query/Scopes/Filters/Fields/Terms.php @@ -36,7 +36,9 @@ public function apply($query, $handle, $values) $operator = $values['operator']; match ($operator) { - 'like' => $query->whereJsonContains($handle, $values['term']), + 'like' => $this->fieldtype->config('max_items') === 1 + ? $query->where($handle, 'like', "%{$values['term']}%") + : $query->whereJsonContains($handle, $values['term']), 'null' => $query->whereNull($handle), 'not-null' => $query->whereNotNull($handle), }; From 4efb48ceda3dfdfe582782ba1e363b178e9408c6 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 14 May 2025 19:02:31 +0100 Subject: [PATCH 212/490] [5.x] Ensure asset validation rules are returned as strings to GraphQL (#11781) --- src/Fieldtypes/Assets/DimensionsRule.php | 5 +++ src/Fieldtypes/Assets/ImageRule.php | 5 +++ src/Fieldtypes/Assets/MimesRule.php | 5 +++ src/Fieldtypes/Assets/MimetypesRule.php | 5 +++ src/Fieldtypes/Assets/SizeBasedRule.php | 5 +++ src/GraphQL/Types/FormType.php | 16 +++++++- tests/Feature/GraphQL/FormTest.php | 49 ++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Fieldtypes/Assets/DimensionsRule.php b/src/Fieldtypes/Assets/DimensionsRule.php index d1864601fe0..7d20c4f3621 100644 --- a/src/Fieldtypes/Assets/DimensionsRule.php +++ b/src/Fieldtypes/Assets/DimensionsRule.php @@ -124,4 +124,9 @@ protected function failsRatioCheck($parameters, $width, $height) return abs($numerator / $denominator - $width / $height) > $precision; } + + public function __toString() + { + return 'dimensions:'.implode(',', $this->parameters); + } } diff --git a/src/Fieldtypes/Assets/ImageRule.php b/src/Fieldtypes/Assets/ImageRule.php index eb9955d158e..f96d5aa4b60 100644 --- a/src/Fieldtypes/Assets/ImageRule.php +++ b/src/Fieldtypes/Assets/ImageRule.php @@ -49,4 +49,9 @@ public function message() { return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.image'); } + + public function __toString() + { + return 'image:'.implode(',', $this->parameters); + } } diff --git a/src/Fieldtypes/Assets/MimesRule.php b/src/Fieldtypes/Assets/MimesRule.php index 485ac393cf6..47d9000e5c2 100644 --- a/src/Fieldtypes/Assets/MimesRule.php +++ b/src/Fieldtypes/Assets/MimesRule.php @@ -51,4 +51,9 @@ public function message() { return str_replace(':values', implode(', ', $this->parameters), __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimes')); } + + public function __toString() + { + return 'mimes:'.implode(',', $this->parameters); + } } diff --git a/src/Fieldtypes/Assets/MimetypesRule.php b/src/Fieldtypes/Assets/MimetypesRule.php index ad1c82acaae..24256bbb056 100644 --- a/src/Fieldtypes/Assets/MimetypesRule.php +++ b/src/Fieldtypes/Assets/MimetypesRule.php @@ -46,4 +46,9 @@ public function message() { return str_replace(':values', implode(', ', $this->parameters), __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimetypes')); } + + public function __toString() + { + return 'mimetypes:'.implode(',', $this->parameters); + } } diff --git a/src/Fieldtypes/Assets/SizeBasedRule.php b/src/Fieldtypes/Assets/SizeBasedRule.php index 35338e010c6..e0cbacfeae0 100644 --- a/src/Fieldtypes/Assets/SizeBasedRule.php +++ b/src/Fieldtypes/Assets/SizeBasedRule.php @@ -66,4 +66,9 @@ protected function getFileSize($id) return false; } + + public function __toString() + { + return 'size:'.implode(',', $this->parameters); + } } diff --git a/src/GraphQL/Types/FormType.php b/src/GraphQL/Types/FormType.php index a5bdf502e64..a340ee1a5fa 100644 --- a/src/GraphQL/Types/FormType.php +++ b/src/GraphQL/Types/FormType.php @@ -35,7 +35,21 @@ public function fields(): array 'rules' => [ 'type' => GraphQL::type(ArrayType::NAME), 'resolve' => function ($form, $args, $context, $info) { - return $form->blueprint()->fields()->validator()->rules(); + return collect($form->blueprint()->fields()->validator()->rules()) + ->map(function ($rules) { + return collect($rules)->map(function ($rule) { + if (is_string($rule)) { + return $rule; + } + + if ($rule instanceof \Stringable) { + return (string) $rule; + } + + return $rule; + }); + }) + ->all(); }, ], 'sections' => [ diff --git a/tests/Feature/GraphQL/FormTest.php b/tests/Feature/GraphQL/FormTest.php index 73b0b805519..678b2d8014b 100644 --- a/tests/Feature/GraphQL/FormTest.php +++ b/tests/Feature/GraphQL/FormTest.php @@ -317,4 +317,53 @@ public function it_queries_the_sections() ], ]]); } + + #[Test] + public function it_returns_string_based_validation_rules_for_mimes_mimetypes_dimension_size_and_image() + { + Form::make('contact')->title('Contact Us')->save(); + + $blueprint = Blueprint::makeFromFields([ + 'name' => [ + 'type' => 'assets', + 'display' => 'Asset', + 'validate' => [ + 'mimes:image/jpeg,image/png', + 'mimetypes:image/jpeg,image/png', + 'dimensions:1024', + 'size:1000', + 'image:jpeg', + ], + ], + ]); + + BlueprintRepository::shouldReceive('find')->with('forms.contact')->andReturn($blueprint); + + $query = <<<'GQL' +{ + form(handle: "contact") { + rules + } +} +GQL; + $this + ->withoutExceptionHandling() + ->post('/graphql', ['query' => $query]) + ->assertGqlOk() + ->assertExactJson(['data' => [ + 'form' => [ + 'rules' => [ + 'name' => [ + 'mimes:image/jpeg,image/png', + 'mimetypes:image/jpeg,image/png', + 'dimensions:1024', + 'size:1000', + 'image:jpeg', + 'array', + 'nullable', + ], + ], + ], + ]]); + } } From 50ae5a23b9ac5f22f172b1c6ed828a68c43b0146 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 14 May 2025 14:29:45 -0400 Subject: [PATCH 213/490] [5.x] Asset validation rules as string in GraphQL, part 2 (#11790) --- .../GraphQL/CastableToValidationString.php | 8 ++++++++ src/Fieldtypes/Assets/DimensionsRule.php | 5 +++-- src/Fieldtypes/Assets/ImageRule.php | 5 +++-- src/Fieldtypes/Assets/MimesRule.php | 5 +++-- src/Fieldtypes/Assets/MimetypesRule.php | 5 +++-- src/Fieldtypes/Assets/SizeBasedRule.php | 5 +++-- src/GraphQL/Types/FormType.php | 7 ++++--- tests/Feature/GraphQL/FormTest.php | 17 +++++++++++++++++ 8 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 src/Contracts/GraphQL/CastableToValidationString.php diff --git a/src/Contracts/GraphQL/CastableToValidationString.php b/src/Contracts/GraphQL/CastableToValidationString.php new file mode 100644 index 00000000000..2324206b663 --- /dev/null +++ b/src/Contracts/GraphQL/CastableToValidationString.php @@ -0,0 +1,8 @@ + $precision; } - public function __toString() + public function toGqlValidationString(): string { return 'dimensions:'.implode(',', $this->parameters); } diff --git a/src/Fieldtypes/Assets/ImageRule.php b/src/Fieldtypes/Assets/ImageRule.php index f96d5aa4b60..8044190761b 100644 --- a/src/Fieldtypes/Assets/ImageRule.php +++ b/src/Fieldtypes/Assets/ImageRule.php @@ -3,11 +3,12 @@ namespace Statamic\Fieldtypes\Assets; use Illuminate\Contracts\Validation\Rule; +use Statamic\Contracts\GraphQL\CastableToValidationString; use Statamic\Facades\Asset; use Statamic\Statamic; use Symfony\Component\HttpFoundation\File\UploadedFile; -class ImageRule implements Rule +class ImageRule implements CastableToValidationString, Rule { protected $parameters; @@ -50,7 +51,7 @@ public function message() return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.image'); } - public function __toString() + public function toGqlValidationString(): string { return 'image:'.implode(',', $this->parameters); } diff --git a/src/Fieldtypes/Assets/MimesRule.php b/src/Fieldtypes/Assets/MimesRule.php index 47d9000e5c2..c184a34d578 100644 --- a/src/Fieldtypes/Assets/MimesRule.php +++ b/src/Fieldtypes/Assets/MimesRule.php @@ -3,11 +3,12 @@ namespace Statamic\Fieldtypes\Assets; use Illuminate\Contracts\Validation\Rule; +use Statamic\Contracts\GraphQL\CastableToValidationString; use Statamic\Facades\Asset; use Statamic\Statamic; use Symfony\Component\HttpFoundation\File\UploadedFile; -class MimesRule implements Rule +class MimesRule implements CastableToValidationString, Rule { protected $parameters; @@ -52,7 +53,7 @@ public function message() return str_replace(':values', implode(', ', $this->parameters), __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimes')); } - public function __toString() + public function toGqlValidationString(): string { return 'mimes:'.implode(',', $this->parameters); } diff --git a/src/Fieldtypes/Assets/MimetypesRule.php b/src/Fieldtypes/Assets/MimetypesRule.php index 24256bbb056..65e3e0400bd 100644 --- a/src/Fieldtypes/Assets/MimetypesRule.php +++ b/src/Fieldtypes/Assets/MimetypesRule.php @@ -3,11 +3,12 @@ namespace Statamic\Fieldtypes\Assets; use Illuminate\Contracts\Validation\Rule; +use Statamic\Contracts\GraphQL\CastableToValidationString; use Statamic\Facades\Asset; use Statamic\Statamic; use Symfony\Component\HttpFoundation\File\UploadedFile; -class MimetypesRule implements Rule +class MimetypesRule implements CastableToValidationString, Rule { protected $parameters; @@ -47,7 +48,7 @@ public function message() return str_replace(':values', implode(', ', $this->parameters), __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimetypes')); } - public function __toString() + public function toGqlValidationString(): string { return 'mimetypes:'.implode(',', $this->parameters); } diff --git a/src/Fieldtypes/Assets/SizeBasedRule.php b/src/Fieldtypes/Assets/SizeBasedRule.php index e0cbacfeae0..875c3c612d0 100644 --- a/src/Fieldtypes/Assets/SizeBasedRule.php +++ b/src/Fieldtypes/Assets/SizeBasedRule.php @@ -3,10 +3,11 @@ namespace Statamic\Fieldtypes\Assets; use Illuminate\Contracts\Validation\Rule; +use Statamic\Contracts\GraphQL\CastableToValidationString; use Statamic\Facades\Asset; use Symfony\Component\HttpFoundation\File\UploadedFile; -abstract class SizeBasedRule implements Rule +abstract class SizeBasedRule implements CastableToValidationString, Rule { protected $parameters; @@ -67,7 +68,7 @@ protected function getFileSize($id) return false; } - public function __toString() + public function toGqlValidationString(): string { return 'size:'.implode(',', $this->parameters); } diff --git a/src/GraphQL/Types/FormType.php b/src/GraphQL/Types/FormType.php index a340ee1a5fa..5552eb43e76 100644 --- a/src/GraphQL/Types/FormType.php +++ b/src/GraphQL/Types/FormType.php @@ -3,6 +3,7 @@ namespace Statamic\GraphQL\Types; use Statamic\Contracts\Forms\Form; +use Statamic\Contracts\GraphQL\CastableToValidationString; use Statamic\Facades\GraphQL; use Statamic\Fields\Value; @@ -42,11 +43,11 @@ public function fields(): array return $rule; } - if ($rule instanceof \Stringable) { - return (string) $rule; + if ($rule instanceof CastableToValidationString) { + return $rule->toGqlValidationString(); } - return $rule; + return get_class($rule).'::class'; }); }) ->all(); diff --git a/tests/Feature/GraphQL/FormTest.php b/tests/Feature/GraphQL/FormTest.php index 678b2d8014b..d3ce69622d4 100644 --- a/tests/Feature/GraphQL/FormTest.php +++ b/tests/Feature/GraphQL/FormTest.php @@ -6,6 +6,7 @@ use Facades\Statamic\Fields\BlueprintRepository; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use Statamic\Contracts\GraphQL\CastableToValidationString; use Statamic\Facades\Blueprint; use Statamic\Facades\Form; use Tests\PreventSavingStacheItemsToDisk; @@ -333,6 +334,8 @@ public function it_returns_string_based_validation_rules_for_mimes_mimetypes_dim 'dimensions:1024', 'size:1000', 'image:jpeg', + 'new Tests\Feature\GraphQL\TestValidationRuleWithToString', + 'new Tests\Feature\GraphQL\TestValidationRuleWithoutToString', ], ], ]); @@ -359,6 +362,8 @@ public function it_returns_string_based_validation_rules_for_mimes_mimetypes_dim 'dimensions:1024', 'size:1000', 'image:jpeg', + 'thevalidationrule:foo,bar', + 'Tests\\Feature\\GraphQL\\TestValidationRuleWithoutToString::class', 'array', 'nullable', ], @@ -367,3 +372,15 @@ public function it_returns_string_based_validation_rules_for_mimes_mimetypes_dim ]]); } } + +class TestValidationRuleWithToString implements CastableToValidationString +{ + public function toGqlValidationString(): string + { + return 'thevalidationrule:foo,bar'; + } +} + +class TestValidationRuleWithoutToString +{ +} From b610b04d77629b389c5d9daabfbb13190f51bda3 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 14 May 2025 14:44:53 -0400 Subject: [PATCH 214/490] changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f43de4e83d7..e538473b0a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Release Notes +## 5.55.0 (2025-05-14) + +### What's new +- Middleware to redirect absolute domains ending in dot [#11782](https://github.com/statamic/cms/issues/11782) by @indykoning +- Add `increment`/`decrement` methods to `ContainsData` [#11786](https://github.com/statamic/cms/issues/11786) by @duncanmcclean +- Update GraphiQL [#11780](https://github.com/statamic/cms/issues/11780) by @duncanmcclean +- Allow selecting entries from different sites within the link fieldtype [#10546](https://github.com/statamic/cms/issues/10546) by @justkidding96 +- Ability to select entries from all sites from Bard link [#11768](https://github.com/statamic/cms/issues/11768) by @edalzell + +### What's fixed +- Ensure asset validation rules are returned as strings to GraphQL [#11781](https://github.com/statamic/cms/issues/11781) by @ryanmitchell +- Asset validation rules as string in GraphQL, part 2 [#11790](https://github.com/statamic/cms/issues/11790) by @jasonvarga +- Fix Term filter on entry listing not working when limiting to 1 term [#11735](https://github.com/statamic/cms/issues/11735) by @liucf +- Fix filtering group fieldtype null values [#11788](https://github.com/statamic/cms/issues/11788) by @jacksleight +- Clone internal data collections [#11777](https://github.com/statamic/cms/issues/11777) by @jacksleight +- Fix creating terms in non-default sites [#11746](https://github.com/statamic/cms/issues/11746) by @duncanmcclean +- Ensure `null` values are filtered out in dictionary field config [#11773](https://github.com/statamic/cms/issues/11773) by @duncanmcclean +- Use deep copy of set's data in bard field [#11766](https://github.com/statamic/cms/issues/11766) by @faltjo +- Dutch translations [#11783](https://github.com/statamic/cms/issues/11783) by @rinusvandam +- PHPUnit: Use `#[Test]` attribute instead of `/** @test */` [#11767](https://github.com/statamic/cms/issues/11767) by @duncanmcclean + + + ## 5.54.0 (2025-05-02) ### What's new From 9a5f65a67203ce2901a7178e1769997637ccc2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Prai=C3=9F?= <6369555+ChristianPraiss@users.noreply.github.com> Date: Wed, 14 May 2025 21:29:38 +0200 Subject: [PATCH 215/490] [5.x] Add --header option to static warm command (#11763) Co-authored-by: Jason Varga --- src/Console/Commands/StaticWarm.php | 28 +++++++++++++++++++++-- tests/Console/Commands/StaticWarmTest.php | 19 +++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/Console/Commands/StaticWarm.php b/src/Console/Commands/StaticWarm.php index a7a052df158..55d45dd38ab 100644 --- a/src/Console/Commands/StaticWarm.php +++ b/src/Console/Commands/StaticWarm.php @@ -42,6 +42,7 @@ class StaticWarm extends Command {--include= : Only warm specific URLs} {--exclude= : Exclude specific URLs} {--max-requests= : Maximum number of requests to warm} + {--header=* : Set custom header (e.g. "Authorization: Bearer your_token")} '; protected $description = 'Warms the static cache by visiting all URLs'; @@ -167,8 +168,10 @@ private function getRelativeUri(int $index): string private function requests() { - return $this->uris()->map(function ($uri) { - return new Request('GET', $uri); + $headers = $this->parseHeaders($this->option('header')); + + return $this->uris()->map(function ($uri) use ($headers) { + return new Request('GET', $uri, $headers); })->all(); } @@ -374,4 +377,25 @@ protected function additionalUris(): Collection return $uris->map(fn ($uri) => URL::makeAbsolute($uri)); } + + private function parseHeaders($headerOptions): array + { + $headers = []; + if (empty($headerOptions)) { + return $headers; + } + if (! is_array($headerOptions)) { + $headerOptions = [$headerOptions]; + } + foreach ($headerOptions as $header) { + if (strpos($header, ':') !== false) { + [$key, $value] = explode(':', $header, 2); + $headers[trim($key)] = trim($value); + } else { + $this->line("Warning: Invalid header format: '$header'. Headers should be in 'Key: Value' format."); + } + } + + return $headers; + } } diff --git a/tests/Console/Commands/StaticWarmTest.php b/tests/Console/Commands/StaticWarmTest.php index 344fdff0bca..3520413a93d 100644 --- a/tests/Console/Commands/StaticWarmTest.php +++ b/tests/Console/Commands/StaticWarmTest.php @@ -271,6 +271,25 @@ public static function queueConnectionsProvider() ]; } + #[Test] + public function it_sets_custom_headers_on_requests() + { + config(['statamic.static_caching.strategy' => 'half']); + + $mock = Mockery::mock(\GuzzleHttp\Client::class); + $mock->shouldReceive('send')->andReturnUsing(function ($request) { + $this->assertEquals('Bearer testtoken', $request->getHeaderLine('Authorization')); + $this->assertEquals('Bar', $request->getHeaderLine('X-Foo')); + + return Mockery::mock(\GuzzleHttp\Psr7\Response::class); + }); + $this->app->instance(\GuzzleHttp\Client::class, $mock); + + $this->artisan('statamic:static:warm', [ + '--header' => ['Authorization: Bearer testtoken', 'X-Foo: Bar'], + ])->assertExitCode(0); + } + private function createPage($slug, $attributes = []) { $this->makeCollection()->save(); From 4a797bbfe81629aae15be56441bcd26d8ab8abb1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 19 May 2025 20:34:20 +0100 Subject: [PATCH 216/490] [5.x] Prepare value & operator before passing to Eloquent Query Builder (#11805) --- src/Query/EloquentQueryBuilder.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index e95c29c2aca..ccbeb44ae49 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -167,6 +167,10 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' return $this; } + [$value, $operator] = $this->prepareValueAndOperator( + $value, $operator, func_num_args() === 2 + ); + $this->builder->where($this->column($column), $operator, $value, $boolean); return $this; From a3e543a08304a45e3bdfcbfa374be1def59ac432 Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 20 May 2025 12:46:21 -0500 Subject: [PATCH 217/490] [5.x] Correct issue with nested noparse and partials (#11801) --- .../Language/Runtime/RuntimeParser.php | 7 +++- tests/Antlers/Runtime/NoparseTest.php | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/View/Antlers/Language/Runtime/RuntimeParser.php b/src/View/Antlers/Language/Runtime/RuntimeParser.php index 4283493d4cf..068f3061b84 100644 --- a/src/View/Antlers/Language/Runtime/RuntimeParser.php +++ b/src/View/Antlers/Language/Runtime/RuntimeParser.php @@ -314,6 +314,11 @@ protected function isIgnitionInstalled() return class_exists(ViewException::class) || class_exists('Spatie\LaravelIgnition\Exceptions\ViewException'); } + protected function shouldCacheRenderNodes($text) + { + return ! str_contains($text, '/noparse'); + } + /** * Parses and renders the input text, with the provided runtime data. * @@ -350,7 +355,7 @@ protected function renderText($text, $data = []) $parseText = $this->sanitizePhp($text); $cacheSlug = md5($parseText); - if (! array_key_exists($cacheSlug, self::$standardRenderNodeCache)) { + if (! array_key_exists($cacheSlug, self::$standardRenderNodeCache) || ! $this->shouldCacheRenderNodes($text)) { $this->documentParser->setIsVirtual($this->view == ''); if (strlen($this->view) > 0) { diff --git a/tests/Antlers/Runtime/NoparseTest.php b/tests/Antlers/Runtime/NoparseTest.php index 67befc65498..1f190bd4d6e 100644 --- a/tests/Antlers/Runtime/NoparseTest.php +++ b/tests/Antlers/Runtime/NoparseTest.php @@ -2,11 +2,16 @@ namespace Tests\Antlers\Runtime; +use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState; +use Statamic\View\Antlers\Language\Runtime\NodeProcessor; use Statamic\View\Antlers\Language\Utilities\StringUtilities; use Tests\Antlers\ParserTestCase; +use Tests\FakesViews; class NoparseTest extends ParserTestCase { + use FakesViews; + public function test_noparse_ignores_braces_entirely() { $template = <<<'EOT' @@ -142,4 +147,39 @@ public function test_multiple_noparse_regions() $this->assertSame($expected, StringUtilities::normalizeLineEndings(trim($this->renderString($template, ['title' => 'the title'])))); } + + public function test_noparse_in_nested_partials_renders_correctly() + { + $template = <<<'EOT' + {{ partial:partial_a }} + {{ partial:partial_a }} + {{ noparse }}inside noparse{{ /noparse }} + {{ /partial:partial_a }} + {{ /partial:partial_a }} + + {{ partial:partial_a }} + {{ partial:partial_a }} + {{ noparse }}inside noparse{{ /noparse }} + {{ /partial:partial_a }} + {{ /partial:partial_a }} + + {{ partial:partial_a }} + {{ partial:partial_a }} + {{ noparse }}inside noparse{{ /noparse }} + {{ /partial:partial_a }} + {{ /partial:partial_a }} +EOT; + + GlobalRuntimeState::$peekCallbacks[] = function ($processor, $nodes) { + NodeProcessor::$break = true; + }; + + $this->withFakeViews(); + $this->viewShouldReturnRaw('partial_a', '{{ slot }}'); + + $actual = StringUtilities::normalizeLineEndings(trim($this->renderString($template))); + + $occurrences = substr_count($actual, 'inside noparse'); + $this->assertEquals(3, $occurrences, "Expected 'inside noparse' to appear exactly 3 times"); + } } From 8739cd67940eef675a86d859ef9aa77fa70db9e6 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 20 May 2025 18:46:47 +0100 Subject: [PATCH 218/490] [5.x] Add `moveQuietly` method to `Asset` class (#11804) --- src/Assets/Asset.php | 7 ++++++ tests/Assets/AssetTest.php | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index d0d994d8c89..66bed47dc6e 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -775,6 +775,13 @@ public function move($folder, $filename = null) return $this; } + public function moveQuietly($folder, $filename = null) + { + $this->withEvents = false; + + return $this->move(...func_get_args()); + } + /** * Replace an asset and/or its references where necessary. * diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index 8fc4992adf1..2895b14514c 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -1105,6 +1105,54 @@ public function it_can_be_moved_to_another_folder() Event::assertDispatched(AssetSaved::class); } + #[Test] + public function it_can_be_moved_to_another_folder_quietly() + { + Storage::fake('local'); + $disk = Storage::disk('local'); + $disk->put('old/asset.txt', 'The asset contents'); + $container = Facades\AssetContainer::make('test')->disk('local'); + Facades\AssetContainer::shouldReceive('save')->with($container); + Facades\AssetContainer::shouldReceive('findByHandle')->with('test')->andReturn($container); + $asset = $container->makeAsset('old/asset.txt')->data(['foo' => 'bar']); + $asset->save(); + $oldMeta = $disk->get('old/.meta/asset.txt.yaml'); + $disk->assertExists('old/asset.txt'); + $disk->assertExists('old/.meta/asset.txt.yaml'); + $this->assertEquals([ + 'old/asset.txt', + ], $container->files()->all()); + $this->assertEquals([ + 'old/asset.txt' => ['foo' => 'bar'], + ], $container->assets('/', true)->keyBy->path()->map(function ($item) { + return $item->data()->all(); + })->all()); + + Event::fake(); + $return = $asset->moveQuietly('new'); + + $this->assertEquals($asset, $return); + $disk->assertMissing('old/asset.txt'); + $disk->assertMissing('old/.meta/asset.txt.yaml'); + $disk->assertExists('new/asset.txt'); + $disk->assertExists('new/.meta/asset.txt.yaml'); + $this->assertEquals($oldMeta, $disk->get('new/.meta/asset.txt.yaml')); + $this->assertEquals([ + 'new/asset.txt', + ], $container->files()->all()); + $this->assertEquals([ + 'new/asset.txt' => ['foo' => 'bar'], + ], $container->assets('/', true)->keyBy->path()->map(function ($item) { + return $item->data()->all(); + })->all()); + $this->assertEquals([ + 'old', // the empty directory doesnt actually get deleted + 'new', + 'new/asset.txt', + ], $container->contents()->cached()->keys()->all()); + Event::assertNotDispatched(AssetSaved::class); + } + #[Test] public function it_can_be_moved_to_another_folder_with_a_new_filename() { From 116f21406a717914df39c24dc11bcb835a448120 Mon Sep 17 00:00:00 2001 From: Adel Date: Tue, 20 May 2025 21:51:20 +0400 Subject: [PATCH 219/490] [5.x] Fix facade PhpDocs for better understanding by Laravel Idea (#11798) --- src/Facades/Blink.php | 2 +- src/Facades/Blueprint.php | 2 +- src/Facades/Collection.php | 2 +- src/Facades/Entry.php | 6 +++--- src/Facades/Fieldset.php | 2 +- src/Facades/Form.php | 2 +- src/Facades/FormSubmission.php | 2 +- src/Facades/GlobalSet.php | 2 +- src/Facades/GlobalVariables.php | 2 +- src/Facades/Revision.php | 2 +- src/Facades/Role.php | 2 +- src/Facades/Search.php | 2 +- src/Facades/StaticCache.php | 2 +- src/Facades/Structure.php | 2 +- src/Facades/Taxonomy.php | 2 +- src/Facades/Term.php | 2 +- src/Facades/User.php | 2 +- src/Facades/UserGroup.php | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Facades/Blink.php b/src/Facades/Blink.php index dcc8aec4d8d..9563cf79e51 100644 --- a/src/Facades/Blink.php +++ b/src/Facades/Blink.php @@ -24,7 +24,7 @@ * @method static mixed once($key, callable $callable) * @method static mixed|\Spatie\Blink\Blink store($name = 'default') * - * @see Statamic\Support\Blink + * @see \Statamic\Support\Blink */ class Blink extends Facade { diff --git a/src/Facades/Blueprint.php b/src/Facades/Blueprint.php index 39d105a70e9..72dcf3743e3 100644 --- a/src/Facades/Blueprint.php +++ b/src/Facades/Blueprint.php @@ -25,7 +25,7 @@ * @method static \Illuminate\Support\Collection getAdditionalNamespaces() * * @see \Statamic\Fields\BlueprintRepository - * @see \Statamic\Fields\Blueprint + * @link \Statamic\Fields\Blueprint */ class Blueprint extends Facade { diff --git a/src/Facades/Collection.php b/src/Facades/Collection.php index 56dc341589b..9d499b99cc2 100644 --- a/src/Facades/Collection.php +++ b/src/Facades/Collection.php @@ -22,7 +22,7 @@ * @method static \Illuminate\Support\Collection getComputedCallbacks($collection) * * @see CollectionRepository - * @see \Statamic\Entries\Collection + * @link \Statamic\Entries\Collection */ class Collection extends Facade { diff --git a/src/Facades/Entry.php b/src/Facades/Entry.php index c5d05e54b17..6d3e7b8d037 100644 --- a/src/Facades/Entry.php +++ b/src/Facades/Entry.php @@ -26,9 +26,9 @@ * @method static void updateParents(\Statamic\Entries\Collection $collection, $ids = null) * * @see \Statamic\Stache\Repositories\EntryRepository - * @see \Statamic\Stache\Query\EntryQueryBuilder - * @see \Statamic\Entries\EntryCollection - * @see \Statamic\Entries\Entry + * @link \Statamic\Stache\Query\EntryQueryBuilder + * @link \Statamic\Entries\EntryCollection + * @link \Statamic\Entries\Entry */ class Entry extends Facade { diff --git a/src/Facades/Fieldset.php b/src/Facades/Fieldset.php index 07262a9f891..9afbe0f1e0f 100644 --- a/src/Facades/Fieldset.php +++ b/src/Facades/Fieldset.php @@ -19,7 +19,7 @@ * @method static void addNamespace(string $namespace, string $directory) * * @see \Statamic\Fields\FieldsetRepository - * @see \Statamic\Fields\Fieldset + * @link \Statamic\Fields\Fieldset */ class Fieldset extends Facade { diff --git a/src/Facades/Form.php b/src/Facades/Form.php index e7bf34a7a1a..6e102c2b56d 100644 --- a/src/Facades/Form.php +++ b/src/Facades/Form.php @@ -20,7 +20,7 @@ * @method static ExporterRepository exporters() * * @see \Statamic\Contracts\Forms\FormRepository - * @see \Statamic\Forms\Form + * @link \Statamic\Forms\Form */ class Form extends Facade { diff --git a/src/Facades/FormSubmission.php b/src/Facades/FormSubmission.php index 8a106b7b441..55ec36b6ee1 100644 --- a/src/Facades/FormSubmission.php +++ b/src/Facades/FormSubmission.php @@ -19,7 +19,7 @@ * @method static SubmissionContract make() * * @see \Statamic\Contracts\Forms\SubmissionRepository - * @see \Statamic\Forms\Submission + * @link \Statamic\Forms\Submission */ class FormSubmission extends Facade { diff --git a/src/Facades/GlobalSet.php b/src/Facades/GlobalSet.php index 2e7c1f0c34f..06dd92888cc 100644 --- a/src/Facades/GlobalSet.php +++ b/src/Facades/GlobalSet.php @@ -14,7 +14,7 @@ * @method static void delete(\Statamic\Contracts\Globals\GlobalSet $global) * * @see \Statamic\Stache\Repositories\GlobalRepository - * @see \Statamic\Globals\GlobalSet + * @link \Statamic\Globals\GlobalSet */ class GlobalSet extends Facade { diff --git a/src/Facades/GlobalVariables.php b/src/Facades/GlobalVariables.php index e9fddd3c9ad..cfd4dca66d1 100644 --- a/src/Facades/GlobalVariables.php +++ b/src/Facades/GlobalVariables.php @@ -14,7 +14,7 @@ * @method static void delete(\Statamic\Globals\Variables $variable) * * @see \Statamic\Stache\Repositories\GlobalVariablesRepository - * @see \Statamic\Globals\Variables + * @link \Statamic\Globals\Variables */ class GlobalVariables extends Facade { diff --git a/src/Facades/Revision.php b/src/Facades/Revision.php index 4ca25265f51..6826d1e90b0 100644 --- a/src/Facades/Revision.php +++ b/src/Facades/Revision.php @@ -14,7 +14,7 @@ * @method static void delete(\Statamic\Contracts\Revisions\Revision $revision) * * @see \Statamic\Revisions\RevisionRepository - * @see \Statamic\Revisions\Revision + * @link \Statamic\Revisions\Revision */ class Revision extends Facade { diff --git a/src/Facades/Role.php b/src/Facades/Role.php index 9a4cec914e3..fceb8227203 100644 --- a/src/Facades/Role.php +++ b/src/Facades/Role.php @@ -15,7 +15,7 @@ * @method static void delete(\Statamic\Contracts\Auth\Role $role) * * @see \Statamic\Contracts\Auth\RoleRepository - * @see \Statamic\Auth\Role + * @link \Statamic\Auth\Role */ class Role extends Facade { diff --git a/src/Facades/Search.php b/src/Facades/Search.php index 6fcb6370ddf..54c1eb9227b 100644 --- a/src/Facades/Search.php +++ b/src/Facades/Search.php @@ -15,7 +15,7 @@ * @method static void deleteFromIndexes(Searchable $searchable) * * @see \Statamic\Search\Search - * @see \Statamic\Search\Index + * @link \Statamic\Search\Index */ class Search extends Facade { diff --git a/src/Facades/StaticCache.php b/src/Facades/StaticCache.php index df7aadc73a8..d70550234a3 100644 --- a/src/Facades/StaticCache.php +++ b/src/Facades/StaticCache.php @@ -21,7 +21,7 @@ * @method static void includeJs() * * @see StaticCacheManager - * @see Cacher + * @link Cacher */ class StaticCache extends Facade { diff --git a/src/Facades/Structure.php b/src/Facades/Structure.php index a5923b1af70..f5cb2dd9b28 100644 --- a/src/Facades/Structure.php +++ b/src/Facades/Structure.php @@ -13,7 +13,7 @@ * @method static void delete(Structure $structure) * * @see \Statamic\Contracts\Structures\StructureRepository - * @see \Statamic\Structures\Structure + * @link \Statamic\Structures\Structure */ class Structure extends Facade { diff --git a/src/Facades/Taxonomy.php b/src/Facades/Taxonomy.php index 1b2c20966be..1abd25406a8 100644 --- a/src/Facades/Taxonomy.php +++ b/src/Facades/Taxonomy.php @@ -20,7 +20,7 @@ * @method static additionalPreviewTargets(string $handle) * * @see \Statamic\Stache\Repositories\TaxonomyRepository - * @see \Statamic\Taxonomies\Taxonomy + * @link \Statamic\Taxonomies\Taxonomy */ class Taxonomy extends Facade { diff --git a/src/Facades/Term.php b/src/Facades/Term.php index 11d366bb837..83ac310c9c1 100644 --- a/src/Facades/Term.php +++ b/src/Facades/Term.php @@ -24,7 +24,7 @@ * @method static \Illuminate\Support\Collection applySubstitutions($items) * * @see \Statamic\Contracts\Taxonomies\TermRepository - * @see \Statamic\Taxonomies\Term + * @link \Statamic\Taxonomies\Term */ class Term extends Facade { diff --git a/src/Facades/User.php b/src/Facades/User.php index 1737cd4443a..2baae604780 100644 --- a/src/Facades/User.php +++ b/src/Facades/User.php @@ -24,7 +24,7 @@ * @method static void computed(string|array $field, ?\Closure $callback = null) * * @see \Statamic\Contracts\Auth\UserRepository - * @see \Statamic\Auth\User + * @link \Statamic\Auth\User */ class User extends Facade { diff --git a/src/Facades/UserGroup.php b/src/Facades/UserGroup.php index 3cb41ce9b21..e9ee0305cd1 100644 --- a/src/Facades/UserGroup.php +++ b/src/Facades/UserGroup.php @@ -14,7 +14,7 @@ * @method static \Statamic\Fields\Blueprint blueprint() * * @see \Statamic\Contracts\Auth\UserGroupRepository - * @see \Statamic\Auth\UserGroup + * @link \Statamic\Auth\UserGroup */ class UserGroup extends Facade { From 90d0ff46602976f8bc4c7872e053efacd1798d1a Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 20 May 2025 13:04:26 -0500 Subject: [PATCH 220/490] [5.x] Corrects Antlers error logging with PHP nodes (#11800) --- .../Language/Runtime/GlobalRuntimeState.php | 1 + .../Language/Runtime/NodeProcessor.php | 6 +- tests/Antlers/Runtime/PhpEnabledTest.php | 80 ++++++++++++++++++- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php b/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php index 9e5f2d24d99..75799219c9e 100644 --- a/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php +++ b/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php @@ -216,6 +216,7 @@ public static function mergeTagRuntimeAssignments($assignments) public static function resetGlobalState() { + self::$templateFileStack = []; self::$shareVariablesTemplateTrigger = ''; self::$layoutVariables = []; self::$containsLayout = false; diff --git a/src/View/Antlers/Language/Runtime/NodeProcessor.php b/src/View/Antlers/Language/Runtime/NodeProcessor.php index b5dc8a5eaad..a5b05d1764b 100644 --- a/src/View/Antlers/Language/Runtime/NodeProcessor.php +++ b/src/View/Antlers/Language/Runtime/NodeProcessor.php @@ -1218,7 +1218,9 @@ public function reduce($processNodes) 'User content Antlers PHP tag.' ); } else { - Log::warning('PHP Node evaluated in user content: '.$node->name->name, [ + $logContent = $node->rawStart.$node->innerContent().$node->rawEnd; + + Log::warning('PHP Node evaluated in user content: '.$logContent, [ 'file' => GlobalRuntimeState::$currentExecutionFile, 'trace' => GlobalRuntimeState::$templateFileStack, 'content' => $node->innerContent(), @@ -2456,7 +2458,7 @@ protected function evaluatePhp($buffer) protected function evaluateAntlersPhpNode(PhpExecutionNode $node) { - if (! GlobalRuntimeState::$allowPhpInContent == false && GlobalRuntimeState::$isEvaluatingUserData) { + if (! GlobalRuntimeState::$allowPhpInContent && GlobalRuntimeState::$isEvaluatingUserData) { return StringUtilities::sanitizePhp($node->content); } diff --git a/tests/Antlers/Runtime/PhpEnabledTest.php b/tests/Antlers/Runtime/PhpEnabledTest.php index d5e051fb65d..cd1122defbb 100644 --- a/tests/Antlers/Runtime/PhpEnabledTest.php +++ b/tests/Antlers/Runtime/PhpEnabledTest.php @@ -2,9 +2,13 @@ namespace Tests\Antlers\Runtime; +use Illuminate\Support\Facades\Log; use PHPUnit\Framework\Attributes\Test; +use Statamic\Fields\Field; use Statamic\Fields\Fieldtype; use Statamic\Fields\Value; +use Statamic\Fieldtypes\Text; +use Statamic\View\Antlers\Language\Runtime\GlobalRuntimeState; use Statamic\View\Antlers\Language\Runtime\RuntimeConfiguration; use Statamic\View\Antlers\Language\Utilities\StringUtilities; use Tests\Antlers\ParserTestCase; @@ -513,8 +517,8 @@ public function test_php_node_assignments_within_loops() public function test_assignments_from_php_nodes() { $template = <<<'EOT' -{{? - $value_one = 100; +{{? + $value_one = 100; $value_two = 0; ?}} @@ -533,4 +537,76 @@ public function test_assignments_from_php_nodes() $this->assertStringContainsString('', $result); $this->assertStringContainsString('', $result); } + + public function test_disabled_php_echo_node_inside_user_values() + { + $textFieldtype = new Text(); + $field = new Field('text_field', [ + 'type' => 'text', + 'antlers' => true, + ]); + + $textContent = <<<'TEXT' +Text: {{$ Str::upper('hello, world.') $}} +TEXT; + + $textFieldtype->setField($field); + $value = new Value($textContent, 'text_field', $textFieldtype); + + Log::shouldReceive('warning') + ->once() + ->with("PHP Node evaluated in user content: {{\$ Str::upper('hello, world.') \$}}", [ + 'file' => null, + 'trace' => [], + 'content' => " Str::upper('hello, world.') ", + ]); + + $result = $this->renderString('{{ text_field }}', ['text_field' => $value]); + + $this->assertSame('Text: ', $result); + + GlobalRuntimeState::$allowPhpInContent = true; + + $result = $this->renderString('{{ text_field }}', ['text_field' => $value]); + + $this->assertSame('Text: HELLO, WORLD.', $result); + + GlobalRuntimeState::$allowPhpInContent = false; + } + + public function test_disabled_php_node_inside_user_values() + { + $textFieldtype = new Text(); + $field = new Field('text_field', [ + 'type' => 'text', + 'antlers' => true, + ]); + + $textContent = <<<'TEXT' +Text: {{? echo Str::upper('hello, world.') ?}} +TEXT; + + $textFieldtype->setField($field); + $value = new Value($textContent, 'text_field', $textFieldtype); + + Log::shouldReceive('warning') + ->once() + ->with("PHP Node evaluated in user content: {{? echo Str::upper('hello, world.') ?}}", [ + 'file' => null, + 'trace' => [], + 'content' => " echo Str::upper('hello, world.') ", + ]); + + $result = $this->renderString('{{ text_field }}', ['text_field' => $value]); + + $this->assertSame('Text: ', $result); + + GlobalRuntimeState::$allowPhpInContent = true; + + $result = $this->renderString('{{ text_field }}', ['text_field' => $value]); + + $this->assertSame('Text: HELLO, WORLD.', $result); + + GlobalRuntimeState::$allowPhpInContent = false; + } } From 8cc8f927026b579af44d0ef4267cbc6bdae93387 Mon Sep 17 00:00:00 2001 From: Andreas Schantl Date: Tue, 20 May 2025 20:10:29 +0200 Subject: [PATCH 221/490] [5.x] Fix storing submissions of forms with 'files' fieldtypes even when disabled (#11794) --- src/Forms/DeleteTemporaryAttachments.php | 4 +++- tests/Forms/SendEmailsTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Forms/DeleteTemporaryAttachments.php b/src/Forms/DeleteTemporaryAttachments.php index 9d49785f52c..2905f270cd0 100644 --- a/src/Forms/DeleteTemporaryAttachments.php +++ b/src/Forms/DeleteTemporaryAttachments.php @@ -31,6 +31,8 @@ public function handle() $this->submission->remove($field->handle()); }); - $this->submission->saveQuietly(); + if ($this->submission->form()->store()) { + $this->submission->saveQuietly(); + } } } diff --git a/tests/Forms/SendEmailsTest.php b/tests/Forms/SendEmailsTest.php index 45371c97a3f..0360d85f685 100644 --- a/tests/Forms/SendEmailsTest.php +++ b/tests/Forms/SendEmailsTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Bus; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Statamic\Contracts\Forms\SubmissionRepository; use Statamic\Facades\Form as FacadesForm; use Statamic\Facades\Site; use Statamic\Forms\DeleteTemporaryAttachments; @@ -115,6 +116,29 @@ public function it_dispatches_delete_attachments_job_after_dispatching_email_job ]); } + #[Test] + public function delete_attachments_job_only_saves_submission_when_enabled() + { + $form = tap(FacadesForm::make('attachments_test')->email([ + 'from' => 'first@sender.com', + 'to' => 'first@recipient.com', + 'foo' => 'bar', + ]))->save(); + + $form + ->store(false) + ->blueprint() + ->ensureField('attachments', ['type' => 'files'])->save(); + + $submission = $form->makeSubmission(); + + (new DeleteTemporaryAttachments($submission))->handle(); + + $submissions = app(SubmissionRepository::class)->all(); + + $this->assertEmpty($submissions); + } + #[Test] #[DataProvider('noEmailsProvider')] public function no_email_jobs_are_queued_if_none_are_configured($emailConfig) From 5e08d2611c166e0d0a27093b0f8fec55596ce895 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 20 May 2025 19:34:56 +0100 Subject: [PATCH 222/490] [5.x] Hide read only and computed fields in user creation wizard (#11635) --- src/Http/Controllers/CP/Users/UsersController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Controllers/CP/Users/UsersController.php b/src/Http/Controllers/CP/Users/UsersController.php index 61e24244176..1534aa46c8b 100644 --- a/src/Http/Controllers/CP/Users/UsersController.php +++ b/src/Http/Controllers/CP/Users/UsersController.php @@ -140,6 +140,7 @@ public function create(Request $request) $additional = $fields->all() ->reject(fn ($field) => in_array($field->handle(), ['roles', 'groups', 'super'])) + ->reject(fn ($field) => in_array($field->visibility(), ['read_only', 'computed'])) ->keys(); $viewData = [ From 6fceebd512fba4e0a3bdb527db4003d4bcb991a1 Mon Sep 17 00:00:00 2001 From: Simon <35518922+simonworkhouse@users.noreply.github.com> Date: Wed, 21 May 2025 02:40:56 +0800 Subject: [PATCH 223/490] [5.x] Fix values being wrapped in arrays causing multiple selected options (#11630) Co-authored-by: Kailum --- src/Fieldtypes/HasSelectOptions.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Fieldtypes/HasSelectOptions.php b/src/Fieldtypes/HasSelectOptions.php index 7e1289a3c42..802fd22b69b 100644 --- a/src/Fieldtypes/HasSelectOptions.php +++ b/src/Fieldtypes/HasSelectOptions.php @@ -52,9 +52,8 @@ public function preProcessIndex($value) { $values = $this->preProcess($value); - $values = collect(is_array($values) ? $values : [$values]); - - return $values->map(function ($value) { + // NOTE: Null-coalescing into `[null]` as that matches old behaviour. + return collect($values ?? [null])->map(function ($value) { return $this->getLabel($value); })->all(); } @@ -67,9 +66,8 @@ public function preProcess($value) return []; } - $value = is_array($value) ? $value : [$value]; - - $values = collect($value)->map(function ($value) { + // NOTE: Null-coalescing into `[null]` as that matches old behaviour. + $values = collect($value ?? [null])->map(function ($value) { return $this->config('cast_booleans') ? $this->castFromBoolean($value) : $value; }); From bfd4050233d4ef0612f2d449fce2d64250580e3e Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 20 May 2025 15:06:31 -0400 Subject: [PATCH 224/490] changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e538473b0a9..98d5c42926e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Release Notes +## 5.56.0 (2025-05-20) + +### What's new +- Add `moveQuietly` method to `Asset` class [#11804](https://github.com/statamic/cms/issues/11804) by @duncanmcclean +- Add --header option to static warm command [#11763](https://github.com/statamic/cms/issues/11763) by @ChristianPraiss + +### What's fixed +- Fix values being wrapped in arrays causing multiple selected options [#11630](https://github.com/statamic/cms/issues/11630) by @simonworkhouse +- Hide read only and computed fields in user creation wizard [#11635](https://github.com/statamic/cms/issues/11635) by @duncanmcclean +- Fix storing submissions of forms with 'files' fieldtypes even when disabled [#11794](https://github.com/statamic/cms/issues/11794) by @andjsch +- Corrects Antlers error logging with PHP nodes [#11800](https://github.com/statamic/cms/issues/11800) by @JohnathonKoster +- Fix facade PhpDocs for better understanding by Laravel Idea [#11798](https://github.com/statamic/cms/issues/11798) by @adelf +- Correct issue with nested noparse and partials [#11801](https://github.com/statamic/cms/issues/11801) by @JohnathonKoster +- Prepare value & operator before passing to Eloquent Query Builder [#11805](https://github.com/statamic/cms/issues/11805) by @duncanmcclean + + + ## 5.55.0 (2025-05-14) ### What's new From 2fcf9e32c06d1ac8e4e84eefab33156c4d5af3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Schwei=C3=9Finger?= Date: Thu, 22 May 2025 18:17:39 +0200 Subject: [PATCH 225/490] [5.x] Further increase trackDirtyState timeout (#11811) * Further increase trackDirtyState timeout * Use a more cautious value for trackDirtyState timeout * Increase the timeout everywhere else too --------- Co-authored-by: Duncan McClean --- resources/js/components/entries/PublishForm.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/js/components/entries/PublishForm.vue b/resources/js/components/entries/PublishForm.vue index 2f8d0b9d535..db0d4477da6 100644 --- a/resources/js/components/entries/PublishForm.vue +++ b/resources/js/components/entries/PublishForm.vue @@ -610,7 +610,7 @@ export default { this.trackDirtyState = false this.values = this.resetValuesFromResponse(response.data.data.values); this.extraValues = response.data.data.extraValues; - this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 500) + this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 750) this.$nextTick(() => this.$emit('saved', response)); return; } @@ -635,7 +635,7 @@ export default { this.trackDirtyState = false; this.values = this.resetValuesFromResponse(response.data.data.values); this.extraValues = response.data.data.extraValues; - this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 500); + this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 750); this.initialPublished = response.data.data.published; this.activeLocalization.published = response.data.data.published; this.activeLocalization.status = response.data.data.status; @@ -724,7 +724,7 @@ export default { this.initialPublished = data.values.published; this.readOnly = data.readOnly; - this.trackDirtyStateTimeout = setTimeout(() => this.trackDirtyState = true, 500); // after any fieldtypes do a debounced update + this.trackDirtyStateTimeout = setTimeout(() => this.trackDirtyState = true, 750); // after any fieldtypes do a debounced update }) }, @@ -807,7 +807,7 @@ export default { clearTimeout(this.trackDirtyStateTimeout); this.trackDirtyState = false; this.values = this.resetValuesFromResponse(response.data.data.values); - this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 500); + this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 750); this.activeLocalization.title = response.data.data.title; this.activeLocalization.published = response.data.data.published; this.activeLocalization.status = response.data.data.status; @@ -864,7 +864,7 @@ export default { clearTimeout(this.trackDirtyStateTimeout); this.trackDirtyState = false; this.values = this.resetValuesFromResponse(response.data.values); - this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 500); + this.trackDirtyStateTimeout = setTimeout(() => (this.trackDirtyState = true), 750); this.initialPublished = response.data.published; this.activeLocalization.published = response.data.published; this.activeLocalization.status = response.data.status; From 7be04816733ba3e7b5efd3a661972ace784af972 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 22 May 2025 11:30:22 -0500 Subject: [PATCH 226/490] [5.x] Checks for Closure instances instead of is_callable inside Route::statamic(...) (#11809) Checks for Closure instances instead of is_callable Global functions will return true when using `is_callable` --- src/Http/Controllers/FrontendController.php | 4 ++-- tests/Routing/RoutesTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/FrontendController.php b/src/Http/Controllers/FrontendController.php index f7204225cc7..1023ccbbd12 100644 --- a/src/Http/Controllers/FrontendController.php +++ b/src/Http/Controllers/FrontendController.php @@ -46,9 +46,9 @@ public function route(Request $request, ...$args) $view = Arr::pull($params, 'view'); $data = Arr::pull($params, 'data'); - throw_if(is_callable($view) && $data, new \Exception('Parameter [$data] not supported with [$view] closure!')); + throw_if(($view instanceof Closure) && $data, new \Exception('Parameter [$data] not supported with [$view] closure!')); - if (is_callable($view)) { + if ($view instanceof Closure) { $resolvedView = static::resolveRouteClosure($view, $params); } diff --git a/tests/Routing/RoutesTest.php b/tests/Routing/RoutesTest.php index 1eba7b93c23..b94d924ae4f 100644 --- a/tests/Routing/RoutesTest.php +++ b/tests/Routing/RoutesTest.php @@ -151,6 +151,8 @@ protected function resolveApplicationConfiguration($app) }); }); + + Route::statamic('/callables-test', 'auth'); }); } @@ -596,6 +598,17 @@ public function it_uses_a_non_default_layout() ->assertOk() ->assertSee('Custom layout'); } + + #[Test] + public function it_checks_for_closure_instances_instead_of_callables() + { + $this->viewShouldReturnRaw('auth', 'Hello, world.'); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + + $this->get('/callables-test') + ->assertOk() + ->assertSee('Hello, world.'); + } } class FooClass From 7f87b605f858e4fb2b5c2acd5ab8c83d481c9155 Mon Sep 17 00:00:00 2001 From: Marty Friedel Date: Wed, 28 May 2025 17:59:44 +0930 Subject: [PATCH 227/490] [5.x] Perform null check on data in video fieldtype (#11821) Perform null check on data in video fieldtype --- resources/js/components/fieldtypes/VideoFieldtype.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/js/components/fieldtypes/VideoFieldtype.vue b/resources/js/components/fieldtypes/VideoFieldtype.vue index 0f0e31cb634..dfa20b8bd77 100644 --- a/resources/js/components/fieldtypes/VideoFieldtype.vue +++ b/resources/js/components/fieldtypes/VideoFieldtype.vue @@ -89,7 +89,7 @@ export default { }, isEmbeddable() { - return this.isUrl && this.data.includes('youtube') || this.data.includes('vimeo') || this.data.includes('youtu.be'); + return this.isUrl && this.data?.includes('youtube') || this.data?.includes('vimeo') || this.data?.includes('youtu.be'); }, isInvalid() { @@ -106,10 +106,10 @@ export default { isVideo() { return ! this.isEmbeddable && ( - this.data.includes('.mp4') || - this.data.includes('.ogv') || - this.data.includes('.mov') || - this.data.includes('.webm') + this.data?.includes('.mp4') || + this.data?.includes('.ogv') || + this.data?.includes('.mov') || + this.data?.includes('.webm') ) } }, From 20e76605884016162b535ecbfa0f8eb05acead18 Mon Sep 17 00:00:00 2001 From: Mason Curry Date: Wed, 28 May 2025 04:01:38 -0500 Subject: [PATCH 228/490] [5.x] Static Cache Middleware - skip cache if new header is set (#11817) * feat: skip static cache if header is set * Rename the header --------- Co-authored-by: Duncan McClean --- src/StaticCaching/Middleware/Cache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticCaching/Middleware/Cache.php b/src/StaticCaching/Middleware/Cache.php index 80f86e5ce17..86731fa0e3e 100644 --- a/src/StaticCaching/Middleware/Cache.php +++ b/src/StaticCaching/Middleware/Cache.php @@ -184,6 +184,7 @@ private function shouldBeCached($request, $response) $response->headers->has('X-Statamic-Draft') || $response->headers->has('X-Statamic-Private') || $response->headers->has('X-Statamic-Protected') + || $response->headers->has('X-Statamic-Uncacheable') ) { return false; } From 1a3def4124e78b773c2bca08467e67605652e1d2 Mon Sep 17 00:00:00 2001 From: Guillermo Azurdia Date: Wed, 28 May 2025 04:34:22 -0500 Subject: [PATCH 229/490] [5.x] Add `not_in` parameter to Assets tag (#11820) * Add filterNotIn on Assets tag * Move up the method * wip * Pint * wip * Move next to the other filter methods * It was fine where it was. --------- Co-authored-by: Duncan McClean --- src/Tags/Assets.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Tags/Assets.php b/src/Tags/Assets.php index 9ef77dcc8bc..5789415d394 100644 --- a/src/Tags/Assets.php +++ b/src/Tags/Assets.php @@ -159,6 +159,22 @@ protected function filterByType($value) }); } + /** + * Filter out assets from a requested folder. + * + * @return void + */ + private function filterNotIn() + { + if ($not_in = $this->params->get('not_in')) { + $regex = '#^('.$not_in.')#'; + + $this->assets = $this->assets->reject(function ($path) use ($regex) { + return preg_match($regex, $path); + }); + } + } + /** * Perform the asset lookups. * @@ -193,6 +209,8 @@ protected function assets($urls) private function output() { + $this->filterNotIn(); + $this->sort(); $this->limit(); From bdb86b6f499b34b7a35b9db1acf3015b5046f972 Mon Sep 17 00:00:00 2001 From: Marty Friedel Date: Thu, 29 May 2025 18:15:40 +0930 Subject: [PATCH 230/490] [5.x] Null check for fieldActions within replicator (#11828) Null check for fieldActions within replicator --- resources/js/components/fieldtypes/replicator/Field.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/replicator/Field.vue b/resources/js/components/fieldtypes/replicator/Field.vue index a768b81e3d6..6fb7373d9ef 100644 --- a/resources/js/components/fieldtypes/replicator/Field.vue +++ b/resources/js/components/fieldtypes/replicator/Field.vue @@ -159,7 +159,7 @@ export default { }, shouldShowFieldActions() { - return !this.isInsideConfigFields && this.fieldActions.length > 0; + return !this.isInsideConfigFields && this.fieldActions?.length > 0; }, fieldActions() { From c8773827bc92b5d151eef2cdf4982edd965f0090 Mon Sep 17 00:00:00 2001 From: Emmanuel Beauchamps Date: Thu, 29 May 2025 10:46:28 +0200 Subject: [PATCH 231/490] [5.x] French translations (#11826) Good for 5.56 --- resources/lang/fr/fieldtypes.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lang/fr/fieldtypes.php b/resources/lang/fr/fieldtypes.php index 12caa80209c..b5a48e024ca 100644 --- a/resources/lang/fr/fieldtypes.php +++ b/resources/lang/fr/fieldtypes.php @@ -44,6 +44,7 @@ 'bard.config.section.editor.instructions' => 'Configurez l’apparence et le comportement général de l’éditeur.', 'bard.config.section.links.instructions' => 'Configurez comment les liens sont gérés dans cette instance de Bard.', 'bard.config.section.sets.instructions' => 'Configurez des blocs de champs qui peuvent être insérés n’importe où dans votre contenu Bard.', + 'bard.config.select_across_sites' => 'Autorisez la sélection d’entrées à partir d’autres sites. Cela désactive également les options de localisation sur le frontal. Pour en savoir plus, consultez la [documentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', 'bard.config.smart_typography' => 'Remplacez les styles de texte courants par les caractères typographiques appropriés.', 'bard.config.target_blank' => 'Définissez `target="_blank"` sur tous les liens.', 'bard.config.toolbar_mode' => 'Choisissez le style de barre d’outils que vous préférez.', @@ -176,7 +177,7 @@ 'taggable.config.placeholder' => 'Saisissez et appuyez sur ↩ Entrée', 'taggable.title' => 'Taggable', 'taxonomies.title' => 'Taxonomies', - 'template.config.blueprint' => 'Ajoute une option "Mapper au Blueprint". Apprenez-en plus dans la [documentation](https://statamic.dev/views#inferring-templates-from-entry-blueprints).', + 'template.config.blueprint' => 'Ajoute une option "Mapper au Blueprint". Pour en savoir plus, consultez la [documentation](https://statamic.dev/views#inferring-templates-from-entry-blueprints).', 'template.config.folder' => 'Affichez uniquement les modèles de ce dossier.', 'template.config.hide_partials' => 'Les partiels sont rarement destinés à être utilisés comme modèles.', 'template.title' => 'Template', From 108a906780a39fb8bad5d3e219c1400326dd3373 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 29 May 2025 21:23:53 +0100 Subject: [PATCH 232/490] [5.x] Entries fieldtype: Only show "Allow Creating" option when using stack selector (#11816) Entries fieldtype: Only show "Allow Creating" option when using stack selector mode --- src/Fieldtypes/Entries.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 7da9258a681..6816ccd5f1e 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -95,6 +95,9 @@ protected function configFieldItems(): array 'instructions' => __('statamic::fieldtypes.entries.config.create'), 'type' => 'toggle', 'default' => true, + 'if' => [ + 'mode' => 'default', + ], ], 'collections' => [ 'display' => __('Collections'), From d4b694e5eb844eaf8e512d947d01cdbf20eb8db4 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 29 May 2025 21:25:33 +0100 Subject: [PATCH 233/490] [5.x] Fix editability of nav items without content reference (#11822) * Display URL for nav items without titles * We already have handling for this in a computed value, but non-entry urls weren't getting through. * Add edit option to twirldown as well. --------- Co-authored-by: Jesse Leite --- resources/js/components/navigation/View.vue | 3 +++ src/Structures/TreeBuilder.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/js/components/navigation/View.vue b/resources/js/components/navigation/View.vue index 4681af65975..ac58beecba0 100644 --- a/resources/js/components/navigation/View.vue +++ b/resources/js/components/navigation/View.vue @@ -108,6 +108,9 @@ v-if="isEntryBranch(branch)" :text="__('Edit Entry')" :redirect="branch.edit_url" /> + $page->entry()->blueprint()->handle(), 'title' => $page->entry()->blueprint()->title(), ] : null, - 'url' => $referenceExists ? $page->url() : null, + 'url' => $page->url(), 'edit_url' => $page->editUrl(), 'can_delete' => $referenceExists ? User::current()->can('delete', $page->entry()) : true, 'slug' => $page->slug(), From 37609e4725364689136f09963f92f3fe6e808484 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 30 May 2025 17:34:21 -0400 Subject: [PATCH 234/490] [5.x] Fix create/edit CP nav descendants not properly triggering active status (#11832) * Add failing test coverage. * Allow non-explicitly defined restful descendants when checking active state. --- src/CP/Navigation/NavItem.php | 24 +++++++++++++++++------ tests/CP/Navigation/ActiveNavItemTest.php | 19 ++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/CP/Navigation/NavItem.php b/src/CP/Navigation/NavItem.php index 3d0878a6b52..7e860638293 100644 --- a/src/CP/Navigation/NavItem.php +++ b/src/CP/Navigation/NavItem.php @@ -281,13 +281,21 @@ public function isChild($isChild = null) ->value($isChild); } + /** + * Check if current url is a restful descendant. + */ + protected function currentUrlIsRestfulDescendant(): bool + { + return (bool) Str::endsWith(request()->url(), [ + '/create', + '/edit', + ]); + } + /** * Check if this nav item was ever a child before user preferences were applied. - * - * @param bool|null $isChild - * @return mixed */ - protected function wasOriginallyChild() + protected function wasOriginallyChild(): bool { return (bool) $this->wasOriginallyChild; } @@ -382,8 +390,12 @@ public function isActive() // If the current URL is not explicitly referenced in the CP nav, // and if this item is/was ever a child nav item, // then check against URL heirarchy conventions using regex pattern. - if ($this->currentUrlIsNotExplicitlyReferencedInNav() && $this->wasOriginallyChild()) { - return $this->isActiveByPattern($this->active); + if ($this->currentUrlIsNotExplicitlyReferencedInNav()) { + switch (true) { + case $this->currentUrlIsRestfulDescendant(): + case $this->wasOriginallyChild(): + return $this->isActiveByPattern($this->active); + } } return request()->url() === URL::removeQueryAndFragment($this->url); diff --git a/tests/CP/Navigation/ActiveNavItemTest.php b/tests/CP/Navigation/ActiveNavItemTest.php index 5f344c902c8..ab560f7ddf1 100644 --- a/tests/CP/Navigation/ActiveNavItemTest.php +++ b/tests/CP/Navigation/ActiveNavItemTest.php @@ -196,6 +196,25 @@ public function it_resolves_core_children_closure_and_can_check_when_parent_and_ $this->assertTrue($this->getItemByDisplay($collections->children(), 'Articles')->isActive()); } + #[Test] + public function it_resolves_core_children_closure_and_can_check_when_parent_and_descendant_of_parent_item_is_active() + { + Facades\Collection::make('pages')->title('Pages')->save(); + Facades\Collection::make('articles')->title('Articles')->save(); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/collections/create') + ->assertStatus(200); + + $collections = $this->buildAndGetItem('Content', 'Collections'); + + $this->assertTrue($collections->isActive()); + $this->assertInstanceOf(Collection::class, $collections->children()); + $this->assertFalse($collections->children()->keyBy->display()->get('Pages')->isActive()); + $this->assertFalse($collections->children()->keyBy->display()->get('Articles')->isActive()); + } + #[Test] public function it_resolves_core_children_closure_and_can_check_when_parent_and_descendant_of_child_item_is_active() { From d1372363f91d5adac7afa3c09f830ef93dba54e0 Mon Sep 17 00:00:00 2001 From: Tom Mulroy Date: Tue, 3 Jun 2025 16:32:27 +0800 Subject: [PATCH 235/490] [5.x] Updating Statamic references in language files. (#11835) Updating Statamic references in language files. --- resources/lang/cs/messages.php | 2 +- resources/lang/ja/messages.php | 10 +++++----- resources/lang/zh_TW/messages.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/lang/cs/messages.php b/resources/lang/cs/messages.php index 1fdd1d1dbd2..1d02fa2bb37 100644 --- a/resources/lang/cs/messages.php +++ b/resources/lang/cs/messages.php @@ -178,7 +178,7 @@ 'outpost_error_422' => 'Chyba v komunikaci se statamic.com.', 'outpost_error_429' => 'Příliš mnoho požadavků. Zkuste to znovu později.', 'outpost_issue_try_later' => 'Při komunikaci se službou statamic.com došlo k problému. Zkuste to prosím později.', - 'outpost_license_key_error' => 'Statamicu se nepodařilo dešifrovat poskytnutý soubor licenčního klíče. Prosím stáhněte znovu ze statmic.com.', + 'outpost_license_key_error' => 'Statamicu se nepodařilo dešifrovat poskytnutý soubor licenčního klíče. Prosím stáhněte znovu ze statamic.com.', 'password_protect_enter_password' => 'Zadejte heslo', 'password_protect_incorrect_password' => 'Heslo není správné', 'password_protect_token_invalid' => 'Token není platný', diff --git a/resources/lang/ja/messages.php b/resources/lang/ja/messages.php index e62c9e4c1f2..1945329773d 100644 --- a/resources/lang/ja/messages.php +++ b/resources/lang/ja/messages.php @@ -128,7 +128,7 @@ 'form_configure_title_instructions' => '通常は「お問い合わせ」などの行動喚起です。', 'getting_started_widget_blueprints' => 'ブループリントは、コンテンツの作成と保存に使用されるカスタム フィールドを定義します。', 'getting_started_widget_collections' => 'コレクションには、サイト内のさまざまな種類のコンテンツが含まれます。', - 'getting_started_widget_docs' => 'Statmic の機能を正しく理解して、Statamic について理解しましょう。', + 'getting_started_widget_docs' => 'Statamic の機能を正しく理解して、Statamic について理解しましょう。', 'getting_started_widget_header' => 'スタティックの入門', 'getting_started_widget_intro' => '新しい静的サイトの構築を開始するには、次の手順から始めることをお勧めします。', 'getting_started_widget_navigation' => 'ナビゲーションバーやフッターなどのレンダリングに使用できるリンクのマルチレベルリストを作成します。', @@ -175,9 +175,9 @@ 'navigation_documentation_instructions' => 'ナビゲーションの構築、構成、レンダリングの詳細については、こちらをご覧ください。', 'navigation_link_to_entry_instructions' => 'エントリにリンクを追加します。構成領域で追加のコレクションへのリンクを有効にします。', 'navigation_link_to_url_instructions' => '内部または外部 URL へのリンクを追加します。設定領域のエントリへのリンクを有効にします。', - 'outpost_error_422' => 'statmic.com との通信中にエラーが発生しました。', + 'outpost_error_422' => 'statamic.com との通信中にエラーが発生しました。', 'outpost_error_429' => 'statamic.com へのリクエストが多すぎます。', - 'outpost_issue_try_later' => 'statmic.com との通信で問題が発生しました。後でもう一度試してください。', + 'outpost_issue_try_later' => 'statamic.com との通信で問題が発生しました。後でもう一度試してください。', 'outpost_license_key_error' => 'Statamic は提供されたライセンス キー ファイルを復号化できませんでした。statamic.com から再度ダウンロードしてください。', 'password_protect_enter_password' => 'ロックを解除するにはパスワードを入力してください', 'password_protect_incorrect_password' => 'パスワードが間違っています。', @@ -246,11 +246,11 @@ 'user_groups_title_instructions' => '通常、編集者や写真家などの複数名詞', 'user_wizard_account_created' => 'ユーザーアカウントが作成されました。', 'user_wizard_intro' => 'ユーザーは、コントロール パネル全体で権限、アクセス、機能をカスタマイズする役割に割り当てることができます。', - 'user_wizard_invitation_body' => 'この Web サイトの管理を開始するには、 :siteで新しい Statmic アカウントをアクティブ化します。安全のため、以下のリンクは:expiry時間後に期限切れになります。その後、サイト管理者に新しいパスワードを問い合わせてください。', + 'user_wizard_invitation_body' => 'この Web サイトの管理を開始するには、 :siteで新しい Statamic アカウントをアクティブ化します。安全のため、以下のリンクは:expiry時間後に期限切れになります。その後、サイト管理者に新しいパスワードを問い合わせてください。', 'user_wizard_invitation_intro' => 'アカウントのアクティベーションの詳細を記載したウェルカム電子メールを新しいユーザーに送信します。', 'user_wizard_invitation_share' => 'これらの資格情報をコピーし、好みの方法で:emailで共有します。', 'user_wizard_invitation_share_before' => 'ユーザーを作成すると、好みの方法で:emailで共有するための詳細が提供されます。', - 'user_wizard_invitation_subject' => ':siteで新しい Statmic アカウントをアクティブ化します', + 'user_wizard_invitation_subject' => ':siteで新しい Statamic アカウントをアクティブ化します', 'user_wizard_roles_groups_intro' => 'ユーザーは、コントロール パネル全体で権限、アクセス、機能をカスタマイズする役割に割り当てることができます。', 'user_wizard_super_admin_instructions' => 'スーパー管理者は、コントロール パネル内のすべてを完全に制御し、アクセスできます。この役割を賢明に与えてください。', 'view_more_count' => '表示:count', diff --git a/resources/lang/zh_TW/messages.php b/resources/lang/zh_TW/messages.php index 5d5fe10f5d6..cee73206be6 100644 --- a/resources/lang/zh_TW/messages.php +++ b/resources/lang/zh_TW/messages.php @@ -132,7 +132,7 @@ 'getting_started_widget_header' => '開始使用 Statamic', 'getting_started_widget_intro' => '要開始建構你的新 Statamic 網站,我們建議從這幾個步驟開始:', 'getting_started_widget_navigation' => '建立多層連結列表,可用來組成導航列、底部連結等。', - 'getting_started_widget_pro' => 'Statmic Pro 包含無限使用者帳號、角色、權限、Git 整合、修訂版、多網站、以及更多功能!', + 'getting_started_widget_pro' => 'Statamic Pro 包含無限使用者帳號、角色、權限、Git 整合、修訂版、多網站、以及更多功能!', 'git_disabled' => 'Statamic Git 整合目前已禁用。', 'git_nothing_to_commit' => '無可提交的內容,內容路徑很乾淨!', 'git_utility_description' => '管理 Git 追蹤的內容。', From db1db9f91b9816f2a07dec6e97f12a677cbc8556 Mon Sep 17 00:00:00 2001 From: CapitaineToinon Date: Wed, 4 Jun 2025 13:12:19 +0200 Subject: [PATCH 236/490] [5.x] Added the option to add renderers to markdown parsers (#11827) * Added the option to add renderers to markdown parsers * Avoid shortening renderer to ren * Make the variable names clearer. * Re-order methods * Allow adding single renderers * Update tests --------- Co-authored-by: Duncan McClean --- src/Markdown/Parser.php | 56 ++++++++++++++++++--- tests/Markdown/Fixtures/HeadingRenderer.php | 20 ++++++++ tests/Markdown/Fixtures/LinkRenderer.php | 20 ++++++++ tests/Markdown/ParserTest.php | 50 ++++++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 tests/Markdown/Fixtures/HeadingRenderer.php create mode 100644 tests/Markdown/Fixtures/LinkRenderer.php diff --git a/src/Markdown/Parser.php b/src/Markdown/Parser.php index c2aa8d8cc05..93752d40ad2 100644 --- a/src/Markdown/Parser.php +++ b/src/Markdown/Parser.php @@ -15,6 +15,7 @@ class Parser protected $converter; protected $extensions = []; + protected $renderers = []; protected $config = []; public function __construct(array $config = []) @@ -37,8 +38,12 @@ public function converter(): CommonMarkConverter $env = $converter->getEnvironment(); - foreach ($this->extensions() as $ext) { - $env->addExtension($ext); + foreach ($this->extensions() as $extension) { + $env->addExtension($extension); + } + + foreach ($this->renderers() as $renderer) { + $env->addRenderer(...$renderer); } return $this->converter = $converter; @@ -65,15 +70,50 @@ public function addExtensions(Closure $closure): self public function extensions(): array { - $exts = []; + $extensions = []; foreach ($this->extensions as $closure) { - foreach (Arr::wrap($closure()) as $ext) { - $exts[] = $ext; + foreach (Arr::wrap($closure()) as $extension) { + $extensions[] = $extension; + } + } + + return $extensions; + } + + public function addRenderer(Closure $closure): self + { + $this->converter = null; + + $this->renderers[] = $closure; + + return $this; + } + + public function addRenderers(Closure $closure): self + { + return $this->addRenderer($closure); + } + + public function renderers(): array + { + $renderers = []; + + foreach ($this->renderers as $closure) { + $closureRenderers = $closure(); + + // When the first item isn't an array, assume it's a single + // renderer and wrap it in an array. + if (! is_array($closureRenderers[0])) { + $closureRenderers = [$closureRenderers]; + } + + foreach ($closureRenderers as $renderer) { + $renderers[] = $renderer; } } - return $exts; + return $renderers; } public function withStatamicDefaults() @@ -151,6 +191,10 @@ public function newInstance(array $config = []) $parser->addExtensions($ext); } + foreach ($this->renderers as $renderer) { + $parser->addRenderers($renderer); + } + return $parser; } } diff --git a/tests/Markdown/Fixtures/HeadingRenderer.php b/tests/Markdown/Fixtures/HeadingRenderer.php new file mode 100644 index 00000000000..1d457c4794c --- /dev/null +++ b/tests/Markdown/Fixtures/HeadingRenderer.php @@ -0,0 +1,20 @@ +{$childRenderer->renderNodes($node->children())}"; + } +} diff --git a/tests/Markdown/Fixtures/LinkRenderer.php b/tests/Markdown/Fixtures/LinkRenderer.php new file mode 100644 index 00000000000..c7c0c69a174 --- /dev/null +++ b/tests/Markdown/Fixtures/LinkRenderer.php @@ -0,0 +1,20 @@ +getUrl()}\" title=\"{$node->getTitle()}\">{$childRenderer->renderNodes($node->children())}"; + } +} diff --git a/tests/Markdown/ParserTest.php b/tests/Markdown/ParserTest.php index 32d4f674f12..8e13247b388 100644 --- a/tests/Markdown/ParserTest.php +++ b/tests/Markdown/ParserTest.php @@ -2,6 +2,8 @@ namespace Tests\Markdown; +use League\CommonMark\Extension\CommonMark\Node\Block\Heading; +use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use PHPUnit\Framework\Attributes\Test; use Statamic\Markdown; use Tests\TestCase; @@ -47,6 +49,38 @@ public function it_adds_extensions_using_an_array() $this->assertEquals("

smile 😀 frown 🙁

\n", $this->parser->parse('smile :) frown :(')); } + #[Test] + public function it_adds_a_renderer() + { + $this->assertEquals("

test

\n", $this->parser->parse('[test](http://example.com)')); + + $this->parser->addRenderer(function () { + return [ + Link::class, + new Fixtures\LinkRenderer, + ]; + }); + + $this->assertEquals("

test

\n", $this->parser->parse('[test](http://example.com)')); + } + + #[Test] + public function it_adds_renderers_using_an_array() + { + $this->assertEquals("

test

\n", $this->parser->parse('[test](http://example.com)')); + $this->assertEquals("

Hello world

\n", $this->parser->parse('# Hello world')); + + $this->parser->addRenderers(function () { + return [ + [Link::class, new Fixtures\LinkRenderer], + [Heading::class, new Fixtures\HeadingRenderer], + ]; + }); + + $this->assertEquals("

test

\n", $this->parser->parse('[test](http://example.com)')); + $this->assertEquals("

Hello world

\n", $this->parser->parse('# Hello world')); + } + #[Test] public function it_creates_a_new_instance_based_on_the_current_instance() { @@ -54,6 +88,13 @@ public function it_creates_a_new_instance_based_on_the_current_instance() return new Fixtures\SmileyExtension; }); + $this->parser->addRenderer(function () { + return [ + Link::class, + new Fixtures\LinkRenderer, + ]; + }); + $this->assertEquals("\n", $this->parser->config('renderer/block_separator')); $this->assertEquals("\n", $this->parser->config('renderer/inner_separator')); $this->assertEquals('allow', $this->parser->config('html_input')); @@ -71,11 +112,20 @@ public function it_creates_a_new_instance_based_on_the_current_instance() return new Fixtures\FrownyExtension; }); + $newParser->addRenderer(function () { + return [ + Heading::class, + new Fixtures\HeadingRenderer, + ]; + }); + $this->assertNotSame($this->parser, $newParser); $this->assertEquals("\n", $newParser->config('renderer/block_separator')); $this->assertEquals('foo', $newParser->config('renderer/inner_separator')); $this->assertEquals('strip', $newParser->config('html_input')); $this->assertCount(2, $newParser->extensions()); $this->assertCount(1, $this->parser->extensions()); + $this->assertCount(2, $newParser->renderers()); + $this->assertCount(1, $this->parser->renderers()); } } From 7da187c6c8e76f116bd4425e8067e3667551b1ce Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 4 Jun 2025 12:57:35 +0100 Subject: [PATCH 237/490] [5.x] Add renderer methods to `Markdown` facade docblock (#11845) Add renderer methods to `Markdown` facade docblock --- src/Facades/Markdown.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Facades/Markdown.php b/src/Facades/Markdown.php index 22d7b23c0ec..e2f99ab24bf 100644 --- a/src/Facades/Markdown.php +++ b/src/Facades/Markdown.php @@ -17,6 +17,9 @@ * @method static Parser addExtension(\Closure $closure) * @method static Parser addExtensions(\Closure $closure) * @method static array extensions() + * @method static Parser addRenderer(\Closure $closure) + * @method static Parser addRenderers(\Closure $closure) + * @method static array renderers() * @method static void withStatamicDefaults() * @method static Parser withAutoLinks() * @method static Parser withAutoLineBreaks() From 83f5d7f4081c608f179461fd2c259ad3fcf548be Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 4 Jun 2025 15:04:53 +0100 Subject: [PATCH 238/490] [5.x] Fix edition check in outpost (#11843) Fix free editions with offline license validation --- src/Licensing/Outpost.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Licensing/Outpost.php b/src/Licensing/Outpost.php index 4385a2c0784..e3e3fa07848 100644 --- a/src/Licensing/Outpost.php +++ b/src/Licensing/Outpost.php @@ -104,7 +104,7 @@ private function licenseKeyFileResponse() Addon::all() ->reject(fn ($addon) => array_key_exists($addon->package(), $response['packages'])) ->mapWithKeys(fn ($addon) => [$addon->package() => [ - 'valid' => ! $addon->isCommercial(), + 'valid' => ! $addon->isCommercial() || $addon->edition() === 'free', 'exists' => $addon->existsOnMarketplace(), 'version_limit' => null, ]]) From e37a91e485058e755f0d98851aab9b48ed1357c6 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 4 Jun 2025 17:44:06 +0100 Subject: [PATCH 239/490] [5.x] Throw 404 response when OAuth provider doesn't exist (#11844) * Throw 404 response when OAuth provider doesn't exist * Throw 404 on callback route as well --- src/Http/Controllers/OAuthController.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Http/Controllers/OAuthController.php b/src/Http/Controllers/OAuthController.php index ac98813dfac..4c69c561dd6 100644 --- a/src/Http/Controllers/OAuthController.php +++ b/src/Http/Controllers/OAuthController.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Two\InvalidStateException; +use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\OAuth; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -17,6 +18,10 @@ public function redirectToProvider(Request $request, string $provider) $referer = $request->headers->get('referer'); $guard = config('statamic.users.guards.web', 'web'); + if (! OAuth::providers()->has($provider)) { + throw new NotFoundHttpException(); + } + if (Str::startsWith(parse_url($referer)['path'], Str::ensureLeft(config('statamic.cp.route'), '/'))) { $guard = config('statamic.users.guards.cp', 'web'); } @@ -30,6 +35,10 @@ public function handleProviderCallback(Request $request, string $provider) { $oauth = OAuth::provider($provider); + if (! $oauth) { + throw new NotFoundHttpException(); + } + try { $providerUser = $oauth->getSocialiteUser(); } catch (InvalidStateException $e) { From eba307923eb5fe9ce7b0954bf6ceecc0357be8c1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 4 Jun 2025 18:04:56 +0100 Subject: [PATCH 240/490] changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d5c42926e..36540c46da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Release Notes +## 5.57.0 (2025-06-04) + +### What's new +- Added the option to add renderers to markdown parsers [#11827](https://github.com/statamic/cms/issues/11827) by @CapitaineToinon +- Add renderer methods to `Markdown` facade docblock [#11845](https://github.com/statamic/cms/issues/11845) by @duncanmcclean +- Add `not_in` parameter to Assets tag [#11820](https://github.com/statamic/cms/issues/11820) by @nopticon +- Added `X-Statamic-Uncacheable` header to prevent responses being statically cached [#11817](https://github.com/statamic/cms/issues/11817) by @macaws + +### What's fixed +- Throw 404 response when OAuth provider doesn't exist [#11844](https://github.com/statamic/cms/issues/11844) by @duncanmcclean +- Fix edition check in outpost [#11843](https://github.com/statamic/cms/issues/11843) by @duncanmcclean +- Updated Statamic references in language files [#11835](https://github.com/statamic/cms/issues/11835) by @tommulroy +- Fix create/edit CP nav descendants not properly triggering active status [#11832](https://github.com/statamic/cms/issues/11832) by @jesseleite +- Fix editability of nav items without content reference [#11822](https://github.com/statamic/cms/issues/11822) by @duncanmcclean +- Entries fieldtype: Only show "Allow Creating" option when using stack selector [#11816](https://github.com/statamic/cms/issues/11816) by @duncanmcclean +- French translations [#11826](https://github.com/statamic/cms/issues/11826) by @ebeauchamps +- Added null check for fieldActions within replicator [#11828](https://github.com/statamic/cms/issues/11828) by @martyf +- Perform null check on data in video fieldtype [#11821](https://github.com/statamic/cms/issues/11821) by @martyf +- Added checks for `Closure` instances instead of `is_callable` inside `Route::statamic(...)` [#11809](https://github.com/statamic/cms/issues/11809) by @JohnathonKoster +- Further increase trackDirtyState timeout [#11811](https://github.com/statamic/cms/issues/11811) by @simonerd + + + ## 5.56.0 (2025-05-20) ### What's new From a084bc122f545ede7839402fb9b075940a7a1bb5 Mon Sep 17 00:00:00 2001 From: Adam Patterson Date: Tue, 10 Jun 2025 11:48:28 -0600 Subject: [PATCH 241/490] [5.x] Remove single quote in Asset upload (#11858) --- src/Assets/AssetUploader.php | 2 ++ tests/Assets/AssetUploaderTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Assets/AssetUploader.php b/src/Assets/AssetUploader.php index dc3ce3936c8..731e5693045 100644 --- a/src/Assets/AssetUploader.php +++ b/src/Assets/AssetUploader.php @@ -89,6 +89,8 @@ public static function getSafeFilename($string) '?' => '-', '*' => '-', '%' => '-', + "'" => '-', + '--' => '-', ]; return (string) Str::of(urldecode($string)) diff --git a/tests/Assets/AssetUploaderTest.php b/tests/Assets/AssetUploaderTest.php index 540e411db63..3e2ec8ec5da 100644 --- a/tests/Assets/AssetUploaderTest.php +++ b/tests/Assets/AssetUploaderTest.php @@ -31,6 +31,8 @@ public static function filenameReplacementsProvider() 'question marks' => ['one?two.jpg', 'one-two.jpg'], 'asterisks' => ['one*two.jpg', 'one-two.jpg'], 'percentage' => ['one%two.jpg', 'one-two.jpg'], + 'single quote' => ["one'two'three.jpg", 'one-two-three.jpg'], + 'double dash' => ['one--two--three.jpg', 'one-two-three.jpg'], 'ascii' => ['fòô-bàř', 'foo-bar'], ]; } From 14cd46b70d8242ac5d6927ad432a9881364e735f Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Tue, 10 Jun 2025 19:51:31 +0200 Subject: [PATCH 242/490] [5.x] Ensure Glide treats asset urls starting with the app url as internal assets (#11839) Co-authored-by: Duncan McClean --- src/Assets/AssetContainer.php | 5 +---- src/Tags/Glide.php | 4 ++++ tests/Assets/AssetContainerTest.php | 15 --------------- tests/Tags/GlideTest.php | 23 +++++++++++++++++++++-- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/Assets/AssetContainer.php b/src/Assets/AssetContainer.php index 86263865e7e..29765fce499 100644 --- a/src/Assets/AssetContainer.php +++ b/src/Assets/AssetContainer.php @@ -27,7 +27,6 @@ use Statamic\Facades\Stache; use Statamic\Facades\URL; use Statamic\Support\Arr; -use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; class AssetContainer implements Arrayable, ArrayAccess, AssetContainerContract, Augmentable @@ -139,9 +138,7 @@ public function url() return null; } - $url = (string) Str::of($this->disk()->url('/')) - ->rtrim('/') - ->after(config('app.url')); + $url = rtrim($this->disk()->url('/'), '/'); return ($url === '') ? '/' : $url; } diff --git a/src/Tags/Glide.php b/src/Tags/Glide.php index f34deb64d53..33276631126 100644 --- a/src/Tags/Glide.php +++ b/src/Tags/Glide.php @@ -279,6 +279,10 @@ private function normalizeItem($item) return $item; } + if (Str::startsWith($item, config('app.url'))) { + $item = Str::after($item, config('app.url')); + } + // External URLs are already fine as-is. if (Str::startsWith($item, ['http://', 'https://'])) { return $item; diff --git a/tests/Assets/AssetContainerTest.php b/tests/Assets/AssetContainerTest.php index 9bc8f92ffed..507c7358792 100644 --- a/tests/Assets/AssetContainerTest.php +++ b/tests/Assets/AssetContainerTest.php @@ -137,21 +137,6 @@ public function it_gets_the_url_from_the_disk_config_when_its_relative() $this->assertEquals('http://localhost/container', $container->absoluteUrl()); } - #[Test] - public function it_gets_the_url_from_the_disk_config_when_its_app_url() - { - config(['filesystems.disks.test' => [ - 'driver' => 'local', - 'root' => __DIR__.'/__fixtures__/container', - 'url' => 'http://localhost/container', - ]]); - - $container = (new AssetContainer)->disk('test'); - - $this->assertEquals('/container', $container->url()); - $this->assertEquals('http://localhost/container', $container->absoluteUrl()); - } - #[Test] public function its_private_if_the_disk_has_no_url() { diff --git a/tests/Tags/GlideTest.php b/tests/Tags/GlideTest.php index 69bac8773ec..aae0616e1e4 100644 --- a/tests/Tags/GlideTest.php +++ b/tests/Tags/GlideTest.php @@ -67,6 +67,20 @@ public function it_outputs_a_data_url() $this->assertStringStartsWith('data:image/jpeg;base64', (string) Parse::template($tag, ['foo' => 'bar.jpg'])); } + #[Test] + #[DefineEnvironment('absoluteHttpRouteUrlWithoutCache')] + /** + * https://github.com/statamic/cms/pull/11839 + */ + public function it_treats_assets_urls_starting_with_the_app_url_as_internal_assets() + { + $this->createImageInPublicDirectory(); + + $result = (string) Parse::template('{{ glide:foo width="100" }}', ['foo' => 'http://localhost/glide/bar.jpg']); + + $this->assertStringStartsWith('/img/glide/bar.jpg', $result); + } + public function relativeRouteUrl($app) { $this->configureGlideCacheDiskWithUrl($app, '/glide'); @@ -77,12 +91,17 @@ public function absoluteHttpRouteUrl($app) $this->configureGlideCacheDiskWithUrl($app, 'http://localhost/glide'); } + public function absoluteHttpRouteUrlWithoutCache($app) + { + $this->configureGlideCacheDiskWithUrl($app, 'http://localhost/glide', false); + } + public function absoluteHttpsRouteUrl($app) { $this->configureGlideCacheDiskWithUrl($app, 'https://localhost/glide'); } - private function configureGlideCacheDiskWithUrl($app, $url) + private function configureGlideCacheDiskWithUrl($app, $url, $cache = 'glide') { $app['config']->set('filesystems.disks.glide', [ 'driver' => 'local', @@ -90,7 +109,7 @@ private function configureGlideCacheDiskWithUrl($app, $url) 'url' => $url, 'visibility' => 'public', ]); - $app['config']->set('statamic.assets.image_manipulation.cache', 'glide'); + $app['config']->set('statamic.assets.image_manipulation.cache', $cache); } private function createImageInPublicDirectory() From 25fd6967509b6a3e890e55a04924c287ffc181e4 Mon Sep 17 00:00:00 2001 From: Martin Dub <49438284+martinoak@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:39:46 +0200 Subject: [PATCH 243/490] [5.x] Prevent null in strtolower() (#11869) --- src/Query/EloquentQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index ccbeb44ae49..f4ff60b852a 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -160,7 +160,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' return $this->whereNested($column, $boolean); } - if (strtolower($operator) == 'like') { + if ($operator !== null && strtolower($operator) == 'like') { $grammar = $this->builder->getConnection()->getQueryGrammar(); $this->builder->whereRaw('LOWER('.$grammar->wrap($this->column($column)).') LIKE ?', strtolower($value), $boolean); From ffbc6c1ae09cb792901f0816df1acddc2d94bac9 Mon Sep 17 00:00:00 2001 From: Simon <35518922+simonworkhouse@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:40:57 +0800 Subject: [PATCH 244/490] [5.x] Updated `AddonServiceProvider::shouldBootRootItems()` to support trailing slashes (#11861) --- src/Providers/AddonServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Providers/AddonServiceProvider.php b/src/Providers/AddonServiceProvider.php index 2743636846c..bd2e65efb87 100644 --- a/src/Providers/AddonServiceProvider.php +++ b/src/Providers/AddonServiceProvider.php @@ -863,8 +863,8 @@ private function shouldBootRootItems() // i.e. It's the "root" provider. If it's in a subdirectory maybe the developer // is organizing their providers. Things like tags etc. can be autoloaded but // root level things like routes, views, config, blueprints, etc. will not. - $thisDir = Path::tidy(dirname((new \ReflectionClass(static::class))->getFileName())); - $autoloadDir = $addon->directory().$addon->autoload(); + $thisDir = Str::ensureRight(Path::tidy(dirname((new \ReflectionClass(static::class))->getFileName())), '/'); + $autoloadDir = Str::ensureRight($addon->directory().$addon->autoload(), '/'); return $thisDir === $autoloadDir; } From 6afaf7b4b6d9ca12a4b93a806e3a42692a7d3b08 Mon Sep 17 00:00:00 2001 From: Adam Patterson Date: Tue, 17 Jun 2025 02:22:08 -0600 Subject: [PATCH 245/490] [5.x] Fixes typo (#11876) Fixes typo --- config/assets.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/assets.php b/config/assets.php index 2ed388c0320..e3587e20c58 100644 --- a/config/assets.php +++ b/config/assets.php @@ -76,7 +76,7 @@ |-------------------------------------------------------------------------- | | You may define global defaults for all manipulation parameters, such as - | quality, format, and sharpness. These can and will be be overwritten + | quality, format, and sharpness. These can and will be overwritten | on the tag parameter level as well as the preset level. | */ From 75f75036de023b9cd2926ca2c7f209c7f5860128 Mon Sep 17 00:00:00 2001 From: Karl <108818570+karlromets@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:03:05 +0300 Subject: [PATCH 246/490] [5.x] Add Estonian translations (#11886) * Add 'et' in CorePreferences.php * Run 'php translator generate et' * Run 'php translator translate et' * Translation fixes in et.json * Translation fixes across php resources * Fix missing newlines at the end of multiple Estonian language files --- resources/lang/et.json | 1096 +++++++++++++++++++ resources/lang/et/dictionary-countries.php | 283 +++++ resources/lang/et/dictionary-currencies.php | 123 +++ resources/lang/et/fieldtypes.php | 208 ++++ resources/lang/et/markdown.php | 57 + resources/lang/et/messages.php | 258 +++++ resources/lang/et/moment.php | 18 + resources/lang/et/permissions.php | 88 ++ resources/lang/et/validation.php | 156 +++ src/Preferences/CorePreferences.php | 1 + 10 files changed, 2288 insertions(+) create mode 100644 resources/lang/et.json create mode 100644 resources/lang/et/dictionary-countries.php create mode 100644 resources/lang/et/dictionary-currencies.php create mode 100644 resources/lang/et/fieldtypes.php create mode 100644 resources/lang/et/markdown.php create mode 100644 resources/lang/et/messages.php create mode 100644 resources/lang/et/moment.php create mode 100644 resources/lang/et/permissions.php create mode 100644 resources/lang/et/validation.php diff --git a/resources/lang/et.json b/resources/lang/et.json new file mode 100644 index 00000000000..fb7d16861a2 --- /dev/null +++ b/resources/lang/et.json @@ -0,0 +1,1096 @@ +{ + "1 update available|:count updates available": ":count uuendus saadaval|:count uuendust saadaval", + "1 update|:count updates": ":count uuendus|:count uuendust", + ":count item selected|:count items selected": ":count valitud üksus|:count valitud üksust", + ":count row|:count rows": ":count rida|:count rida", + ":count selected|:count selected": ":count valitud|:count valitud", + ":count set|:count sets": ":count komplekt|:count komplekti", + ":count word|:count words": ":count sõna|:count sõna", + ":count/:max selected": ":count/:max valitud", + ":count/:total characters": ":count/:total tähemärki", + ":file uploaded": ":file üles laaditud", + ":start-:end of :total": ":start-:end kokku :total-st", + ":success/:total entries were deleted": ":success/:total kirjet kustutati", + ":success/:total entries were published": ":success/:total kirjet avaldati", + ":success/:total entries were unpublished": ":success/:total kirjet peideti", + ":success/:total items were deleted": ":success/:total üksust kustutati", + ":title Field": ":title väli", + "A blueprint with that name already exists.": "Selle nimega mall juba eksisteerib.", + "A fieldset with that name already exists.": "Selle nimega väljakomplekt juba eksisteerib.", + "A Global Set with that handle already exists.": "Sellise pidemega globaalne komplekt juba eksisteerib.", + "A navigation with that handle already exists.": "Sellise pidemega navigeerimine juba eksisteerib.", + "A Role with that handle already exists.": "Sellise pidemega roll juba eksisteerib.", + "A User Group with that handle already exists.": "Sellise pidemega kasutajagrupp juba eksisteerib.", + "A valid blueprint is required.": "Vajalik on kehtiv mall.", + "Above": "Ülal", + "Action completed": "Tegevus lõpetatud", + "Action failed": "Tegevus nurjus", + "Activate Account": "Aktiveeri konto", + "Activation URL": "Aktiveerimise URL", + "Active": "Aktiivne", + "Add": "Lisa", + "Add Attribute": "Lisa atribuut", + "Add child link to entry": "Lisa kirjele alamlink", + "Add child nav item": "Lisa alamnavigatsioonielement", + "Add Color": "Lisa värv", + "Add Column": "Lisa veerg", + "Add Column After": "Lisa veerg pärast", + "Add Column Before": "Lisa veerg enne", + "Add Condition": "Lisa tingimus", + "Add Date": "Lisa kuupäev", + "Add Email": "Lisa e-post", + "Add Item": "Lisa objekt", + "Add Key": "Lisa võti", + "Add Nav Item": "Lisa navigeerimismenüü üksus", + "Add Option": "Lisa valik", + "Add or drag fields here": "Lisa või lohista väljad siia", + "Add Row": "Lisa rida", + "Add Row After": "Lisa rida pärast", + "Add Row Before": "Lisa rida enne", + "Add Row Label": "Lisa rea silt", + "Add Rule": "Lisa reegel", + "Add Ruler": "Lisa joonlaud", + "Add Section": "Lisa jaotis", + "Add Set": "Lisa komplekt", + "Add Set Group": "Lisa komplekti grupp", + "Add Set Label": "Lisa komplekti silt", + "Add Site": "Lisa sait", + "Add Tab": "Lisa vaheleht", + "Added": "Lisatud", + "Addon Settings": "Lisade seaded", + "Addons": "Lisad", + "Affected files": "Mõjutatud failid", + "After": "Pärast", + "After Saving": "Pärast salvestamist", + "Alias Item": "Pseudonüümiüksus", + "Align Center": "Keskele joondamine", + "Align Justify": "Rööpjoondus", + "Align Left": "Vasakule joondamine", + "Align Right": "Paremale joondamine", + "All": "Kõik", + "All caches cleared.": "Kõik vahemälud tühjendatud.", + "All of the following conditions pass": "Kõik järgmised tingimused on täidetud", + "All rights reserved.": "Kõik õigused reserveeritud.", + "Allow additions": "Luba lisamised", + "Allow Antlers": "Luba Antlers", + "Allow Any Color": "Luba suvaline värv", + "Allow Creating": "Luba loomine", + "Allow Downloading": "Luba allalaadimine", + "Allow Fullscreen Mode": "Luba täisekraanirežiim", + "Allow Moving": "Luba liigutamine", + "Allow Renaming": "Luba ümbernimetamine", + "Allow Source Mode": "Luba lähtekoodi režiim", + "Allow Uploads": "Luba üleslaadimised", + "Alt Text": "Alternatiivtekst", + "Always Save": "Salvesta alati", + "Always show": "Näita alati", + "Always Show Set Button": "Kuva alati komplekti nupp", + "An entry will be deleted|:count entries will be deleted": "Kirje kustutatakse|:count kirjet kustutatakse", + "An item with this ID could not be found": "Selle ID-ga üksust ei leitud", + "and": "ja", + "and :count more": "ja veel :count", + "Antlers": "Antlers", + "Any of the following conditions pass": "Mõni järgmistest tingimustest on täidetud", + "Any unsaved changes will not be duplicated into the new entry.": "Salvestamata muudatusi uude kirjesse ei kopeerita.", + "Any unsaved changes will not be reflected in this action's behavior.": "Salvestamata muudatused ei kajastu selle tegevuse käitumises.", + "Appearance": "Välimus", + "Appearance & Behavior": "Välimus ja käitumine", + "Append": "Lisa lõppu", + "Application Cache": "Rakenduse vahemälu", + "Application cache cleared.": "Rakenduse vahemälu tühjendati.", + "Apply": "Rakenda", + "Apply Link": "Rakenda link", + "Are you sure you want to delete this column?": "Oled kindel, et soovid selle veeru kustutada?", + "Are you sure you want to delete this entry?": "Oled kindel, et soovid selle kirje kustutada?", + "Are you sure you want to delete this item?": "Oled kindel, et soovid selle üksuse kustutada?", + "Are you sure you want to delete this row?": "Oled kindel, et soovid selle rea kustutada?", + "Are you sure you want to delete this value?": "Oled kindel, et soovid selle väärtuse kustutada?", + "Are you sure you want to delete this view?": "Oled kindel, et soovid selle vaate kustutada?", + "Are you sure you want to delete this?|Are you sure you want to delete these :count items?": "Oled kindel, et soovid selle kustutada?|Oled kindel, et soovid kustutada need :count üksust?", + "Are you sure you want to move this asset?|Are you sure you want to move these :count assets?": "Oled kindel, et soovid seda vara teisaldada?|Oled kindel, et soovid neid :count vara teisaldada?", + "Are you sure you want to move this folder?|Are you sure you want to move these :count folders?": "Oled kindel, et soovid selle kausta teisaldada?|Oled kindel, et soovid neid :count kausta teisaldada?", + "Are you sure you want to publish this entry?|Are you sure you want to publish these :count entries?": "Oled kindel, et soovid selle kirje avaldada?|Oled kindel, et soovid need :count kirjet avaldada?", + "Are you sure you want to remove this page?": "Oled kindel, et soovid selle lehe eemaldada?", + "Are you sure you want to remove this section and all of its children?": "Oled kindel, et soovid selle jaotise ja kõik selle alamjaotised eemaldada?", + "Are you sure you want to rename this asset?|Are you sure you want to rename these :count assets?": "Oled kindel, et soovid seda vara ümber nimetada?|Oled kindel, et soovid neid :count vara ümber nimetada?", + "Are you sure you want to rename this folder?|Are you sure you want to rename these :count folders?": "Oled kindel, et soovid selle kausta ümber nimetada?|Oled kindel, et soovid neid :count kausta ümber nimetada?", + "Are you sure you want to reset nav customizations?": "Oled kindel, et soovid lähtestada navigeerimise kohandused?", + "Are you sure you want to reset this item?": "Oled kindel, et soovid selle üksuse lähtestada?", + "Are you sure you want to restore this revision?": "Oled kindel, et soovid selle redaktsiooni taastada?", + "Are you sure you want to run this action?|Are you sure you want to run this action on :count items?": "Oled kindel, et soovid selle toimingu käivitada?|Oled kindel, et soovid selle toimingu käivitada :count üksusel?", + "Are you sure you want to unpublish this entry?|Are you sure you want to unpublish these :count entries?": "Oled kindel, et soovid selle kirje avaldamise tühistada?|Oled kindel, et soovid nende :count kirje avaldamise tühistada?", + "Are you sure?": "Oled kindel?", + "Are you sure? This field's value will be replaced by the value in the original entry.": "Oled kindel? Selle välja väärtus asendatakse algse kirje väärtusega.", + "Are you sure? Unsaved changes will be lost.": "Oled kindel? Salvestamata muudatused lähevad kaotsi.", + "Ascending": "Kasvav", + "Asset": "Vara", + "Asset container created": "Vara konteiner loodud", + "Asset container deleted": "Vara konteiner kustutatud", + "Asset container saved": "Vara konteiner salvestatud", + "Asset container updated": "Vara konteiner uuendatud", + "Asset Containers": "Vara konteinerid", + "Asset deleted": "Vara kustutatud", + "Asset folder deleted": "Vara kaust kustutatud", + "Asset folder saved": "Vara kaust salvestatud", + "Asset references updated": "Vara viited uuendatud", + "Asset reuploaded": "Vara uuesti üles laaditud", + "Asset saved": "Vara salvestatud", + "Asset uploaded": "Vara üles laaditud", + "Assets": "Varad", + "Assign Groups": "Määra grupid", + "Assign groups to this user?|Assign groups to these :count users?": "Määra sellele kasutajale grupid?|Määra nendele :count kasutajale grupid?", + "Assign Roles": "Määra rollid", + "Assign roles to this user?|Assign roles to these :count users?": "Määra rollid sellele kasutajale?|Määra rollid nendele :count kasutajale?", + "Assign|Assign to :count users": "Määra|Määra :count kasutajale", + "Attachments": "Manused", + "Author": "Autor", + "Autocomplete": "Automaatne lõpetamine", + "Automatic Line Breaks": "Automaatsed reavahetused", + "Automatic Links": "Automaatsed lingid", + "Available Columns": "Saadaolevad veerud", + "Back to Users": "Tagasi kasutajate juurde", + "BCC Recipient(s)": "Pimekoopia saaja(d)", + "Before": "Enne", + "Before you can delete this fieldset, you need to remove references to it in blueprints and fieldsets:": "Enne selle väljakomplekti kustutamist pead eemaldama selle viited mallidest ja väljakomplektidest:", + "Behavior": "Käitumine", + "Below": "All", + "Between": "Vahel", + "Blockquote": "Plokktsitaat", + "Blueprint": "Mall", + "Blueprint created": "Mall loodud", + "Blueprint deleted": "Mall kustutatud", + "Blueprint reset": "Mall lähtestatud", + "Blueprint saved": "Mall salvestatud", + "Blueprints": "Mallid", + "Blueprints successfully reordered": "Mallid edukalt ümber järjestatud", + "Bold": "Rasvane", + "Border": "Ääris", + "Boundaries": "Piirid", + "Browse": "Sirvi", + "Build time": "Koosteaeg", + "Button": "Nupp", + "Buttons": "Nupud", + "Buttons & Controls": "Nupud ja juhtnupud", + "Buy Licenses": "Osta litsentse", + "Cache": "Vahemälu", + "Cache Manager": "Vahemälu haldur", + "Cached images": "Vahemällu salvestatud pildid", + "Cancel": "Tühista", + "Cast Booleans": "Teisenda tõeväärtusteks", + "CC Recipient(s)": "Koopia saaja(d)", + "Change Password": "Muuda parooli", + "Change successful.": "Muutmine õnnestus.", + "Changes to this field in the fieldset will stay in sync.": "Selle välja muudatused väljakomplektis jäävad sünkroonis.", + "Changes to this fieldset will stay in sync.": "Selle väljakomplekti muudatused jäävad sünkroonis.", + "Character Limit": "Märkide piirang", + "Characters": "Tähemärgid", + "Checkbox Options": "Märkeruudu valikud", + "Choose Blueprint": "Vali mall", + "Choose Image": "Vali pilt", + "Choose...": "Vali...", + "Clear": "Tühjenda", + "Clear All": "Tühjenda kõik", + "Clearable": "Tühjendatav", + "Close": "Sulge", + "Close Editor": "Sulge redaktor", + "Close Markdown Cheatsheet": "Sulge Markdowni spikker", + "Close Modal": "Sulge modaalaken", + "Code Block": "Koodiplokk", + "Collapse": "Ahenda", + "Collapse All": "Ahenda kõik", + "Collapse All Sets": "Ahenda kõik komplektid", + "Collapse Set": "Ahenda komplekt", + "Collection": "Kogumik", + "Collection already exists": "Kogumik on juba olemas", + "Collection created": "Kogumik loodud", + "Collection deleted": "Kogumik kustutatud", + "Collection is not available on site \":handle\".": "Kogumik pole saidil \":handle\" saadaval.", + "Collection saved": "Kogumik salvestatud", + "Collection tree deleted": "Kogumiku puu kustutatud", + "Collection tree saved": "Kogumiku puu salvestatud", + "Collections": "Kogumikud", + "Columns": "Veerud", + "Columns have been reset to their defaults.": "Veerud on lähtestatud vaikeseadetele.", + "Commit": "Edasta", + "Common": "Üldine", + "Computed": "Arvutatud", + "Conditions": "Tingimused", + "Configuration": "Konfiguratsioon", + "Configuration is cached": "Konfiguratsioon on vahemälus", + "Configure": "Seadista", + "Configure Asset Container": "Seadista varakonteiner", + "Configure Blueprints": "Seadista malle", + "Configure Collection": "Seadista kogumikku", + "Configure Form": "Seadista vormi", + "Configure Global Set": "Seadista globaalset komplekti", + "Configure Navigation": "Seadista navigeerimist", + "Configure Role": "Seadista rolli", + "Configure Site": "Seadista saiti", + "Configure Sites": "Seadista saite", + "Configure Taxonomy": "Seadista taksonoomiat", + "Configure User Group": "Seadista kasutajagruppi", + "Confirm": "Kinnita", + "Confirm Password": "Kinnita parool", + "Container": "Konteiner", + "contains": "sisaldab", + "Contains": "Sisaldab", + "contains any": "sisaldab mõnda", + "Content": "Sisu", + "Content committed": "Sisu edastatud", + "Content Model": "Sisu mudel", + "Content saved": "Sisu salvestatud", + "Content Stache": "Sisu Stache", + "Continue Editing": "Jätka redigeerimist", + "Copied to clipboard": "Kopeeritud lõikelauale", + "Copy": "Kopeeri", + "Copy password reset email for this user?": "Kopeeri selle kasutaja parooli lähtestamise e-kiri?", + "Copy Password Reset Link": "Kopeeri parooli lähtestamise link", + "Copy to clipboard": "Kopeeri lõikelauale", + "Copy URL": "Kopeeri URL", + "Core": "Tuum", + "Couldn't publish entry": "Kirje avaldamine ebaõnnestus", + "Couldn't save entry": "Kirje salvestamine ebaõnnestus", + "Couldn't save term": "Termini salvestamine ebaõnnestus", + "Couldn't unpublish entry": "Kirje peitmine ebaõnnestus", + "CP Nav Preferences": "CP navigeerimise eelistused", + "Create": "Loo", + "Create & Link Item": "Loo ja lingi objekt", + "Create a Blueprint": "Loo mall", + "Create a Collection": "Loo kogumik", + "Create a Navigation": "Loo navigeerimine", + "Create and Send Email": "Loo ja saada e-kiri", + "Create Another": "Loo veel üks", + "Create Asset Container": "Loo varakonteiner", + "Create Blueprint": "Loo mall", + "Create Child Entry": "Loo alamkirje", + "Create Collection": "Loo kogumik", + "Create Container": "Loo konteiner", + "Create Entry": "Loo kirje", + "Create Field": "Loo väli", + "Create Fieldset": "Loo väljakomplekt", + "Create Folder": "Loo kaust", + "Create Folders": "Loo kaustad", + "Create Form": "Loo vorm", + "Create Global Set": "Loo globaalne komplekt", + "Create Group": "Loo grupp", + "Create Localization": "Loo lokaliseerimine", + "Create Navigation": "Loo navigeerimine", + "Create New View": "Loo uus vaade", + "Create Revision": "Loo redaktsioon", + "Create Role": "Loo roll", + "Create Taxonomy": "Loo taksonoomia", + "Create Term": "Loo termin", + "Create User": "Loo kasutaja", + "Create User Group": "Loo kasutajagrupp", + "Create Views": "Loo vaated", + "Current": "Praegune", + "Current Password": "Praegune parool", + "Current Version": "Praegune versioon", + "custom": "kohandatud", + "Custom Attributes": "Kohandatud atribuudid", + "Custom Item": "Kohandatud üksus", + "Custom method passes": "Kohandatud meetod läbitud", + "Custom Section": "Kohandatud jaotis", + "Customize Columns": "Kohanda veerge", + "Customize Invitation": "Kohanda kutset", + "Customizing the Control Panel Nav": "Juhtpaneeli navigeerimise kohandamine", + "Dark": "Tume", + "Dark Mode": "Tume režiim", + "Dashboard": "Töölaud", + "Data": "Andmed", + "Data Format": "Andmevorming", + "Data updated": "Andmed uuendatud", + "Date": "Kuupäev", + "Dates & Behaviors": "Kuupäevad ja käitumine", + "Default": "Vaikimisi", + "Default Color": "Vaikimisi värv", + "Default From Address": "Vaikimisi saatja aadress", + "Default From Name": "Vaikimisi saatja nimi", + "Default Mailer": "Vaikimisi meiliprogramm", + "Default Mode": "Vaikimisi režiim", + "Default preferences saved": "Vaikimisi eelistused salvestatud", + "Default Value": "Vaikimisi väärtus", + "Delete": "Kustuta", + "Delete :resource": "Kustuta :resource", + "Delete child entry|Delete :count child entries": "Kustuta alamkirje|Kustuta :count alamkirjet", + "Delete Column": "Kustuta veerg", + "Delete Container": "Kustuta konteiner", + "Delete Entry": "Kustuta kirje", + "Delete Form": "Kustuta vorm", + "Delete Original Asset": "Kustuta algne vara", + "Delete Row": "Kustuta rida", + "Delete Rule": "Kustuta reegel", + "Delete Set": "Kustuta komplekt", + "Delete Table": "Kustuta tabel", + "Delete Taxonomy": "Kustuta taksonoomia", + "Delete User Group": "Kustuta kasutajagrupp", + "Delete Value": "Kustuta väärtus", + "Delete View": "Kustuta vaade", + "Deleted": "Kustutatud", + "Delete|Delete :count items?": "Kustuta|Kustuta :count üksust?", + "Descending": "Kahanev", + "Description of the image": "Pildi kirjeldus", + "Deselect option": "Tühista valik", + "Detach": "Eralda", + "Dictionary": "Sõnastik", + "Directory": "Kataloog", + "Directory already exists.": "Kataloog on juba olemas.", + "Disabled": "Keelatud", + "Discard": "Loobu", + "Discard changes": "Hülga muudatused", + "Disk": "Ketas", + "Dismiss": "Loobu", + "Display": "Kuva", + "Display Label": "Kuva silt", + "Displayed Columns": "Kuvatavad veerud", + "Documentation": "Dokumentatsioon", + "Don't remove empty nodes": "Ära eemalda tühje sõlmi", + "Downgrade to :version": "Taanda versioonile :version", + "Download": "Laadi alla", + "Download file": "Laadi fail alla", + "Downloads": "Allalaadimised", + "Draft": "Mustand", + "Driver": "Draiver", + "Drop File to Upload": "Lohista fail üleslaadimiseks", + "Drop to Upload": "Lohista üleslaadimiseks", + "DummyClass": "Testklass", + "Duplicate": "Dubleeri", + "Duplicate ID Regenerated": "Dubleeritud ID taasloodud", + "Duplicate IDs": "Dubleeritud ID-d", + "Duplicate Row": "Dubleeri rida", + "Duplicate Set": "Dubleeri komplekt", + "Duplicated": "Dubleeritud", + "Dynamic": "Dünaamiline", + "Dynamic Folder": "Dünaamiline kaust", + "e.g. hero_": "nt hero_", + "Earliest Date": "Varaseim kuupäev", + "Edit": "Muuda", + "Edit Asset": "Muuda vara", + "Edit Blueprint": "Muuda malli", + "Edit Blueprints": "Muuda malle", + "Edit Collection": "Muuda kogumikku", + "Edit Container": "Muuda konteinerit", + "Edit Content": "Muuda sisu", + "Edit Entry": "Muuda kirjet", + "Edit Fieldset": "Muuda väljakomplekti", + "Edit Form": "Muuda vormi", + "Edit Global Set": "Muuda globaalset komplekti", + "Edit Image": "Muuda pilti", + "Edit Nav Item": "Muuda navigeerimisüksust", + "Edit nav item": "Muuda navigeerimisüksust", + "Edit Navigation": "Muuda navigeerimist", + "Edit Section": "Muuda jaotist", + "Edit Set": "Muuda komplekti", + "Edit Set Group": "Muuda komplekti gruppi", + "Edit Site": "Muuda saiti", + "Edit Tab": "Muuda vahelehte", + "Edit Taxonomy": "Muuda taksonoomiat", + "Edit Term": "Muuda terminit", + "Edit User": "Muuda kasutajat", + "Edit User Group": "Muuda kasutajagruppi", + "Editable once created": "Pärast loomist muudetav", + "Editions": "Väljaanded", + "Editor": "Redaktor", + "Email": "E-post", + "Email Address": "E-posti aadress", + "Email Content": "E-kirja sisu", + "Email Subject": "E-kirja teema", + "Emojis": "Emotikonid", + "Empty": "Tühi", + "Enable Input Rules": "Luba sisestusreeglid", + "Enable Line Wrapping": "Luba reamurdmine", + "Enable Paste Rules": "Luba kleepimisreeglid", + "Enable Pro Mode": "Luba Pro režiim", + "Enable Publish Dates": "Luba avaldamiskuupäevad", + "Enable Revisions": "Luba redaktsioonid", + "Enable Statamic Pro?": "Kas lubada Statamic Pro?", + "Encryption": "Krüpteerimine", + "Enter any internal or external URL.": "Sisesta mis tahes sise- või välis-URL.", + "Enter URL": "Sisesta URL", + "Entries": "Kirjed", + "Entries could not be deleted": "Kirjete kustutamine ebaõnnestus", + "Entries could not be published": "Kirjete avaldamine ebaõnnestus", + "Entries could not be unpublished": "Kirjete peitmine ebaõnnestus", + "Entries successfully reordered": "Kirjed edukalt ümber järjestatud", + "Entry": "Kirje", + "Entry could not be deleted": "Kirje kustutamine ebaõnnestus", + "Entry could not be published": "Kirje avaldamine ebaõnnestus", + "Entry could not be unpublished": "Kirje peitmine ebaõnnestus", + "Entry created": "Kirje loodud", + "Entry deleted": "Kirje kustutatud", + "Entry deleted|Entries deleted": "Kirje kustutatud|Kirjed kustutatud", + "Entry has a published version": "Kirjel on avaldatud versioon", + "Entry has not been published": "Kirjet pole avaldatud", + "Entry has unpublished changes": "Kirjel on avaldamata muudatusi", + "Entry link": "Kirje link", + "Entry published|Entries published": "Kirje avaldatud|Kirjed avaldatud", + "Entry saved": "Kirje salvestatud", + "Entry schedule reached": "Kirje ajakava täitus", + "Entry unpublished|Entries unpublished": "Kirje peidetud|Kirjed peidetud", + "equals": "võrdub", + "Equals": "Võrdub", + "Escape Markup": "Eira märgistust", + "Everything is up to date.": "Kõik on ajakohane.", + "Example": "Näide", + "Expand": "Laienda", + "Expand All": "Laienda kõik", + "Expand All Sets": "Laienda kõik komplektid", + "Expand Set": "Laienda komplekt", + "Expect a root page": "Eeldatakse juurlehte", + "Expired": "Aegunud", + "Export Submissions": "Ekspordi esitused", + "External link": "Välislink", + "False": "Väär", + "Favorite removed": "Lemmik eemaldatud", + "Favorite saved": "Lemmik salvestatud", + "Favorites": "Lemmikud", + "Featured": "Esiletõstetud", + "Field": "Väli", + "Field added": "Väli lisatud", + "Field Previews": "Väljade eelvaated", + "Fields": "Väljad", + "Fieldset": "Väljakomplekt", + "Fieldset created": "Väljakomplekt loodud", + "Fieldset deleted": "Väljakomplekt kustutatud", + "Fieldset reset": "Väljakomplekt lähtestatud", + "Fieldset saved": "Väljakomplekt salvestatud", + "Fieldsets": "Väljakomplektid", + "Fieldtypes": "Väljatüübid", + "File": "Fail", + "File Driver": "Failidraiver", + "Filename": "Failinimi", + "Filter": "Filter", + "Filter preset deleted": "Filtri eelseade kustutatud", + "Filter preset saved": "Filtri eelseade salvestatud", + "Filter preset updated": "Filtri eelseade uuendatud", + "Finish": "Lõpeta", + "First child": "Esimene alam", + "First Child": "Esimene alam", + "Fix": "Paranda", + "Fixed": "Fikseeritud", + "Floating": "Ujuv", + "Focal Point": "Fookuspunkt", + "Focus Search": "Fookusotsing", + "Folder": "Kaust", + "Folder created": "Kaust loodud", + "Folder Name": "Kausta nimi", + "Forgot password?": "Unustasid parooli?", + "Forgot Your Password?": "Unustasid oma parooli?", + "Form already exists": "Vorm on juba olemas", + "Form created": "Vorm loodud", + "Form deleted": "Vorm kustutatud", + "Form saved": "Vorm salvestatud", + "Form Submission": "Vormi esitus", + "Format": "Vorming", + "Forms": "Vormid", + "Free": "Tasuta", + "From": "Saatja", + "Full Width": "Täislaius", + "Future Date Behavior": "Tulevase kuupäeva käitumine", + "General": "Üldine", + "Generate": "Genereeri", + "Git": "Git", + "Global Search": "Globaalne otsing", + "Global Set created": "Globaalne komplekt loodud", + "Global Set deleted": "Globaalne komplekt kustutatud", + "Global Set saved": "Globaalne komplekt salvestatud", + "Global Sets": "Globaalsed komplektid", + "Global Variables": "Globaalsed muutujad", + "Globals": "Globaalid", + "Go To Listing": "Mine loendisse", + "Greater than": "Suurem kui", + "Greater than or equals": "Suurem või võrdne kui", + "Grid": "Võrk", + "Group": "Grupp", + "Groups": "Grupid", + "Handle": "Pide", + "Heading 1": "Pealkiri 1", + "Heading 2": "Pealkiri 2", + "Heading 3": "Pealkiri 3", + "Heading 4": "Pealkiri 4", + "Heading 5": "Pealkiri 5", + "Heading 6": "Pealkiri 6", + "Heading Anchors": "Pealkirja ankrud", + "Hello!": "Tere!", + "Here": "Siin", + "Hidden": "Peidetud", + "Hidden by default": "Vaikimisi peidetud", + "Hidden from output": "Väljundist peidetud", + "Hidden Item": "Peidetud üksus", + "Hidden Section": "Peidetud jaotis", + "Hide": "Peida", + "Hide Partials": "Peida osalised vaated", + "Hide when": "Peida, kui", + "Horizontal Rule": "Horisontaaljoon", + "Host": "Host", + "HTML view": "HTML vaade", + "HTTP Status": "HTTP staatus", + "I remember my password": "Mäletan oma parooli", + "Icon": "Ikoon", + "ID": "ID", + "ID regenerated and Stache cleared": "ID taasloodud ja Stache tühjendatud", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Kui sul on raskusi nupule \":actionText\" klõpsamisega, kopeeri ja kleebi allolev URL oma veebibrauserisse:", + "Image": "Pilt", + "Image Cache": "Pildi vahemälu", + "Image cache cleared.": "Pildi vahemälu tühjendati.", + "Image Manipulation": "Pilditöötlus", + "Impersonating": "Teisena esinemine", + "Imported from fieldset": "Imporditud väljakomplektist", + "Included in output": "Lisatud väljundisse", + "Indent Size": "Taande suurus", + "Indent Type": "Taande tüüp", + "Index": "Indeks", + "Index Template": "Indeksi mall", + "Inline": "Reasisene", + "Inline Code": "Reasisene kood", + "Inline Label": "Reasisene silt", + "Inline Label when True": "Reasisene silt, kui on tõene", + "Input Behavior": "Sisendi käitumine", + "Input Label": "Sisendi silt", + "Input Type": "Sisendi tüüp", + "Insert Asset": "Sisesta vara", + "Insert Image": "Sisesta pilt", + "Insert Link": "Sisesta link", + "Installed": "Paigaldatud", + "Instructions": "Juhised", + "Instructions Position": "Juhiste asukoht", + "Intelligently Warm Presets": "Arukas eelseadete soojendamine", + "Invalid content, :type button/extension is not enabled": "Vigane sisu, :type nupp/laiendus pole lubatud", + "Invalid content, nodes and marks must have a type": "Vigane sisu, sõlmedel ja märkidel peab olema tüüp", + "Invalid credentials.": "Valed sisselogimisandmed.", + "Invitation": "Kutse", + "Is": "On", + "Isn't": "Ei ole", + "Italic": "Kursiiv", + "Item could not be deleted": "Üksuse kustutamine ebaõnnestus", + "Item deleted|Items deleted": "Üksus kustutatud|Üksused kustutatud", + "Items could not be deleted": "Üksuste kustutamine ebaõnnestus", + "Key": "Võti", + "Key Mappings": "Klahvipaigutused", + "Keyboard Shortcuts": "Klaviatuuri otseteed", + "Keyed": "Võtmega", + "Keys": "Võtmed", + "Label": "Silt", + "Language": "Keel", + "Laptop": "Sülearvuti", + "Last Login": "Viimane sisselogimine", + "Last Modified": "Viimati muudetud", + "Last rebuild": "Viimane koostamine", + "Latest Date": "Viimane kuupäev", + "Layout": "Paigutus", + "Learn more": "Loe lisaks", + "Learn more about :link": "Lisateavet :link kohta", + "Less than": "Väiksem kui", + "Less than or equals": "Väiksem või võrdne kui", + "Licensing": "Litsentsimine", + "Light": "Hele", + "Light Mode": "Hele režiim", + "Link": "Link", + "Link a fieldset": "Lingi väljakomplekt", + "Link a single field": "Lingi üks väli", + "Link Collections": "Lingi kogumikud", + "Link Existing": "Lingi olemasolev", + "Link Existing Item": "Lingi olemasolev objekt", + "Link Fields": "Lingi väljad", + "Link Noopener": "Link Noopener", + "Link Noreferrer": "Link Noreferrer", + "Link to Entry": "Lingi kirjega", + "Link to URL": "Lingi URL-iga", + "Linked fieldset": "Lingitud väljakomplekt", + "Links": "Lingid", + "List": "Nimekiri", + "Listable": "Nimekirjas nähtav", + "Live Preview": "Reaalajas eelvaade", + "Locale": "Lokaal", + "Localizable": "Lokaliseeritav", + "Localizable field": "Lokaliseeritav väli", + "Localizations": "Lokaliseerimised", + "Location": "Asukoht", + "Locked": "Lukustatud", + "Log in": "Logi sisse", + "Log in to continue": "Jätkamiseks logi sisse", + "Log in with :provider": "Logi sisse :provider kaudu", + "Log in with email": "Logi sisse e-postiga", + "Log out": "Logi välja", + "Logged in": "Sisse logitud", + "Login successful.": "Sisselogimine õnnestus.", + "Main": "Peamine", + "Manage Licenses": "Halda litsentse", + "Manage Sets": "Halda komplekte", + "Map to Blueprint": "Seosta malliga", + "Markdown": "Markdown", + "Markdown Cheatsheet": "Markdowni spikker", + "Markdown paths": "Markdowni teed", + "Markdown theme": "Markdowni teema", + "Max": "Maks", + "Max Columns": "Maksimaalne veergude arv", + "Max Depth": "Maksimaalne sügavus", + "Max Files": "Maksimaalne failide arv", + "Max Items": "Maksimaalne üksuste arv", + "Max Rows": "Maksimaalne ridade arv", + "Max Sets": "Maksimaalne komplektide arv", + "Maximum items selected:": "Valitud maksimaalne arv üksusi:", + "Maximum Rows": "Maksimaalne ridade arv", + "Media": "Meedia", + "Merge Cells": "Ühenda lahtrid", + "Min": "Min", + "Min Files": "Minimaalne failide arv", + "Min Rows": "Minimaalne ridade arv", + "Minimum Rows": "Minimaalne ridade arv", + "Miscellaneous": "Mitmesugust", + "Mobile": "Mobiil", + "Modified": "Muudetud", + "Modified Item": "Muudetud üksus", + "Mount": "Paigalda", + "Move": "Teisalda", + "Move Asset|Move :count Assets": "Teisalda vara|Teisalda :count vara", + "Move Folder|Move :count Folders": "Teisalda kaust|Teisalda :count kausta", + "Moved Item": "Teisaldatud üksus", + "Multi-Site": "Mitmikkeelne", + "Multiple": "Mitu", + "My Fieldsets": "Minu väljakomplektid", + "My Nav": "Minu navigeerimine", + "My Preferences": "Minu eelistused", + "Name": "Nimi", + "Nav Item": "Navigatsiooniüksus", + "Navigation": "Navigatsioon", + "Navigation deleted": "Navigatsioon kustutatud", + "Navigation saved": "Navigatsioon salvestatud", + "Navigation tree deleted": "Navigatsioonipuu kustutatud", + "Navigation tree saved": "Navigatsioonipuu salvestatud", + "Never": "Mitte kunagi", + "New Asset": "Uus vara", + "New Filename": "Uus failinimi", + "New Section": "Uus jaotis", + "New Set": "Uus komplekt", + "New Set Group": "Uus komplektigrupp", + "New Tab": "Uus vaheleht", + "Next": "Järgmine", + "No": "Ei", + "No addons installed": "Lisaprogramme pole installitud", + "No available filters": "Filtreid pole saadaval", + "No items with duplicate IDs.": "Dubleeritud ID-ga üksusi pole.", + "No license key": "Litsentsivõti puudub", + "No link data found for": "Lingi andmeid ei leitud:", + "No options to choose from.": "Valikuid pole.", + "No results": "Tulemusi ei leitud", + "No revisions": "Redaktsioone pole", + "No submissions": "Esitusi pole", + "No templates to choose from.": "Malle pole valida.", + "None": "Puudub", + "not": "ei ole", + "Not empty": "Pole tühi", + "Not equals": "Ei võrdu", + "Not Featured": "Pole esile tõstetud", + "Not listable": "Pole nimekirjas nähtav", + "Notes about this revision": "Märkused selle redaktsiooni kohta", + "Number": "Number", + "OK": "OK", + "Only the references will be removed. Entries will not be deleted.": "Eemaldatakse ainult viited. Kirjeid ei kustutata.", + "Open Dropdown": "Ava rippmenüü", + "Open in a new window": "Ava uues aknas", + "Open in new window": "Ava uues aknas", + "Option": "Valik", + "Optional": "Valikuline", + "Options": "Valikud", + "or": "või", + "or drag & drop here to replace.": "või lohista siia asendamiseks.", + "or drag & drop here.": "või lohista siia.", + "Orderable": "Järjestatav", + "Ordered List": "Järjestatud nimekiri", + "Ordering": "Järjestus", + "Origin": "Päritolu", + "Origin Behavior": "Päritolu käitumine", + "Other": "Muu", + "Override Alt": "Kirjuta alternatiivtekst üle", + "Override For Role": "Kirjuta rolli jaoks üle", + "Override For User": "Kirjuta kasutaja jaoks üle", + "Page Not Found": "Lehte ei leitud", + "Pages": "Lehed", + "Parser": "Parser", + "Password": "Parool", + "Password changed": "Parool muudetud", + "Password Confirmation": "Parooli kinnitamine", + "Password for :username": "Parool kasutajale :username", + "Past Date Behavior": "Möödunud kuupäeva käitumine", + "Per Page": "Lehe kohta", + "Per-site": "Saidi kohta", + "Permissions": "Õigused", + "Phone": "Telefon", + "PHP Info": "PHP info", + "PHP Version": "PHP versioon", + "Pick Color": "Vali värv", + "Pin to Favorites": "Kinnita lemmikuks", + "Pin to Top Level": "Kinnita ülatasemele", + "Pinned Item": "Kinnitatud üksus", + "Placeholder": "Kohatäide", + "Pop in": "Sissehüpe", + "Pop out": "Väljahüpe", + "Port": "Port", + "Preferences": "Eelistused", + "Prefix": "Eesliide", + "Prepend": "Lisa algusesse", + "Preview": "Eelvaade", + "Preview Targets": "Eelvaate sihtmärgid", + "Previous": "Eelmine", + "Price": "Hind", + "Pro": "Pro", + "Pro Tip": "Profinõuanne", + "Process Source Images": "Töötle lähtepilte", + "Profile": "Profiil", + "Propagate": "Levita", + "Protected Page": "Kaitstud leht", + "Publish": "Avalda", + "Publish by Default": "Avalda vaikimisi", + "Publish Date": "Avaldamiskuupäev", + "Publish Entry|Publish :count Entries": "Avalda kirje|Avalda :count kirjet", + "Publish Now": "Avalda kohe", + "Published": "Avaldatud", + "Push Tags": "Lükka sildid", + "Query Scopes": "Päringu ulatused", + "Quick Save": "Kiirsalvestus", + "Radio Options": "Raadionupu valikud", + "Range": "Vahemik", + "Read Only": "Ainult lugemiseks", + "Read the Docs": "Loe dokumentatsiooni", + "Read the Documentation": "Loe dokumentatsiooni", + "Reading Time": "Lugemisaeg", + "Rebuild Search": "Ehita otsing uuesti", + "Recipient(s)": "Saaja(d)", + "Records": "Kirjed", + "Redirect": "Ümbersuunamine", + "Refresh": "Värskenda", + "Regards": "Lugupidamisega", + "Regenerate": "Taasta", + "Regenerate from: :field": "Taasta: :field", + "Region": "Piirkond", + "Registration successful.": "Registreerimine õnnestus.", + "Relationship": "Seos", + "Released on :date": "Välja antud :date", + "Remember me": "Jäta mind meelde", + "Remove": "Eemalda", + "Remove All": "Eemalda kõik", + "Remove all empty nodes": "Eemalda kõik tühjad sõlmed", + "Remove Asset": "Eemalda vara", + "Remove child page|Remove :count child pages": "Eemalda alamleht|Eemalda :count alamlehte", + "Remove Empty Nodes": "Eemalda tühjad sõlmed", + "Remove empty nodes at the start and end": "Eemalda tühjad sõlmed algusest ja lõpust", + "Remove Filter": "Eemalda filter", + "Remove Formatting": "Eemalda vormindus", + "Remove Link": "Eemalda link", + "Remove Page": "Eemalda leht", + "Remove tag": "Eemalda silt", + "Rename": "Nimeta ümber", + "Rename Asset|Rename :count Assets": "Nimeta vara ümber|Nimeta :count vara ümber", + "Rename Folder": "Nimeta kaust ümber", + "Rename Folder|Rename :count Folders": "Nimeta kaust ümber|Nimeta :count kausta ümber", + "Rename View": "Nimeta vaade ümber", + "Renamed Section": "Ümbernimetatud jaotis", + "Reorder": "Järjesta ümber", + "Reorderable": "Ümberjärjestatav", + "Replace": "Asenda", + "Replace Asset": "Asenda vara", + "Reply To": "Vasta", + "Repository path": "Repositooriumi tee", + "Require Slugs": "Nõua rööpurleid", + "Required": "Nõutud", + "Reset": "Lähtesta", + "Reset :resource": "Lähtesta :resource", + "Reset Nav Customizations": "Lähtesta navigeerimise kohandused", + "Reset Password": "Lähtesta parool", + "Responsive": "Kohanduv", + "Restore": "Taasta", + "Restore Revision": "Taasta redaktsioon", + "Restrict": "Piira", + "Restrict to Folder": "Piira kaustaga", + "Resume Your Session": "Jätka oma sessiooni", + "Reupload": "Laadi uuesti üles", + "Reveal Password": "Näita parooli", + "Revision created": "Redaktsioon loodud", + "Revision deleted": "Redaktsioon kustutatud", + "Revision History": "Redaktsioonide ajalugu", + "Revision restored": "Redaktsioon taastatud", + "Revision saved": "Redaktsioon salvestatud", + "Revisions": "Redaktsioonid", + "Role": "Roll", + "Role created": "Roll loodud", + "Role deleted": "Roll kustutatud", + "Role saved": "Roll salvestatud", + "Role updated": "Roll uuendatud", + "Roles": "Rollid", + "Roles & Groups": "Rollid ja grupid", + "Roles & Permissions": "Rollid ja õigused", + "Root": "Juur", + "Route": "Marsruut", + "Routing & URLs": "Marsruutimine ja URL-id", + "Rows": "Read", + "Rulers": "Joonlauad", + "Rules": "Reeglid", + "Run action|Run action on :count items": "Käivita toiming|Käivita toiming :count üksusel", + "Save": "Salvesta", + "Save & Publish": "Salvesta ja avalda", + "Save & Unpublish": "Salvesta ja peida", + "Save as HTML": "Salvesta HTML-ina", + "Save Changes": "Salvesta muudatused", + "Save Draft": "Salvesta mustand", + "Save Order": "Salvesta järjekord", + "Save to": "Salvesta asukohta", + "Saved": "Salvestatud", + "Saving": "Salvestan", + "Scaffold Collection": "Loo kogumik", + "Scaffold Views": "Loo vaated", + "Scheduled": "Ajastatud", + "Search": "Otsi", + "Search Index": "Otsinguindeks", + "Search Indexes": "Otsinguindeksid", + "Search Sets": "Otsi komplekte", + "Search...": "Otsi...", + "Searchable": "Otsitav", + "Searchables": "Otsitavad", + "Searching in:": "Otsin:", + "Select": "Vali", + "Select Across Sites": "Vali kõikides saitides", + "Select Collection(s)": "Vali kogumik(ud)", + "Select Dropdown": "Vali rippmenüüst", + "Select Group": "Vali grupp", + "Select Operator": "Vali operaator", + "Select Role": "Vali roll", + "Select Value": "Vali väärtus", + "Selectable Mode": "Valitav režiim", + "Selection": "Valik", + "Seller": "Müüja", + "Send Email Invitation": "Saada e-posti kutse", + "Send Password Reset": "Saada parooli lähtestamine", + "Send password reset email to this user?|Send password reset email to these :count users?": "Saada sellele kasutajale parooli lähtestamise e-kiri?|Saada nendele :count kasutajale parooli lähtestamise e-kiri?", + "Send Test Email": "Saada test-e-kiri", + "Sender": "Saatja", + "Sendmail": "Sendmail", + "Send|Send to :count users": "Saada|Saada :count kasutajale", + "Service Unavailable": "Teenus pole saadaval", + "Session Expired": "Sessioon on aegunud", + "Set Alt": "Määra alternatiivtekst", + "Set as start page": "Määra avaleheks", + "Set Behavior": "Määra käitumine", + "Set to now": "Määra praeguseks", + "Sets": "Komplektid", + "Settings": "Seaded", + "Show": "Näita", + "Show Fields": "Näita välju", + "Show Filename": "Näita failinime", + "Show HTML Source": "Näita HTML lähtekoodi", + "Show Keyboard Shortcuts": "Näita klaviatuuri otseteid", + "Show Line Numbers": "Näita reanumbreid", + "Show Markdown Cheatsheet": "Näita Markdowni spikrit", + "Show Reading Time": "Näita lugemisaega", + "Show Regenerate Button": "Näita taasloomise nuppu", + "Show Seconds": "Näita sekundeid", + "Show Set Alt": "Näita komplekti alternatiivteksti", + "Show Template": "Näita malli", + "Show when": "Näita, kui", + "Show Word Count": "Näita sõnade arvu", + "Shown by default": "Vaikimisi näidatud", + "Single": "Üksik", + "Site": "Sait", + "Site deleted": "Sait kustutatud", + "Site saved": "Sait salvestatud", + "Site selected.": "Sait valitud.", + "Sites": "Saidid", + "Size": "Suurus", + "Slug": "Rööpurl", + "Slugs": "Rööpurlid", + "Small": "Väike", + "Smart Typography": "Arukas tüpograafia", + "Smartypants": "Smartypants", + "Something went wrong": "Midagi läks valesti", + "Sometimes": "Mõnikord", + "Sort Direction": "Sorteerimise suund", + "Sortable": "Sorteeritav", + "Spaces": "Tühikud", + "Special": "Eriline", + "Stache cleared.": "Stache tühjendatud.", + "Stache warmed.": "Stache soojendatud.", + "Stack Selector": "Virna valija", + "Stacked": "Virnastatud", + "Start Impersonating": "Alusta teisena esinemist", + "Start Page": "Avaleht", + "Start typing to search.": "Alusta otsimiseks tippimist.", + "Statamic": "Statamic", + "Statamic Pro is required.": "Vajalik on Statamic Pro.", + "Static Page Cache": "Staatilise lehe vahemälu", + "Static page cache cleared.": "Staatilise lehe vahemälu tühjendati.", + "Status": "Staatus", + "Step": "Samm", + "Stop impersonating": "Lõpeta teisena esinemine", + "Stop Impersonating": "Lõpeta teisena esinemine", + "Store Submissions": "Salvesta esitused", + "Strategy": "Strateegia", + "Strikethrough": "Läbikriipsutus", + "Structured": "Struktureeritud", + "Subject": "Teema", + "Submission deleted": "Esitus kustutatud", + "Submission saved": "Esitus salvestatud", + "Submission successful.": "Esitamine õnnestus.", + "Submissions": "Esitused", + "Submit": "Esita", + "Subscript": "Allindeks", + "Super Admin": "Superadministraator", + "Super User": "Superkasutaja", + "Superscript": "Ülaindeks", + "Support": "Tugi", + "Swatches": "Värvinäidised", + "Sync": "Sünkrooni", + "System": "Süsteem", + "System default": "Süsteemi vaikeväärtus", + "Table": "Tabel", + "Table of Contents": "Sisukord", + "Tablet": "Tahvelarvuti", + "Tabs": "Vahelehed", + "Target Blank": "Sihtmärk _blank", + "Taxonomies": "Taksonoomiad", + "Taxonomy": "Taksonoomia", + "Taxonomy created": "Taksonoomia loodud", + "Taxonomy deleted": "Taksonoomia kustutatud", + "Taxonomy saved": "Taksonoomia salvestatud", + "Template": "Mall", + "Templates": "Mallid", + "Term": "Termin", + "Term created": "Termin loodud", + "Term deleted": "Termin kustutatud", + "Term references updated": "Termini viited uuendatud", + "Term saved": "Termin salvestatud", + "Term Template": "Termini mall", + "Terms": "Terminid", + "Test email sent.": "Test-e-kiri saadetud.", + "Text": "Tekst", + "Text & Rich Content": "Tekst ja rikas sisu", + "Text item": "Tekstiüksus", + "Text view": "Tekstivaade", + "The given data was invalid.": "Esitatud andmed olid vigased.", + "The Statamic Playground": "Statamici mänguväljak", + "Theme": "Teema", + "There are no entries in this collection": "Selles kogumikus pole kirjeid", + "These are now your default columns.": "Need on nüüd sinu vaikimisi veerud.", + "This action is unauthorized.": "See toiming on volitamata.", + "This container is empty": "See konteiner on tühi", + "This form is awaiting responses": "See vorm ootab vastuseid", + "This Global Set has no fields.": "Sellel globaalsel komplektil pole välju.", + "This is now your start page.": "See on nüüd sinu avaleht.", + "This is the published version": "See on avaldatud versioon", + "This is the root page": "See on juurleht", + "This will delete the collection and all of its entries.|This will delete the collections and all of their entries.": "See kustutab kogumiku ja kõik selle kirjed.|See kustutab kogumikud ja kõik nende kirjed.", + "Time Enabled": "Aeg lubatud", + "Timepicker": "Ajavalik", + "Title": "Pealkiri", + "Title Format": "Pealkirja vorming", + "Today": "Täna", + "Toggle": "Lülita", + "Toggle Button": "Lülitusnupp", + "Toggle Dark Mode": "Lülita tume režiim", + "Toggle Fullscreen Mode": "Lülita täisekraanirežiim", + "Toggle Header Cell": "Lülita päiserakk", + "Toggle Mobile Nav": "Lülita mobiilnavigatsioon", + "Toggle Nav": "Lülita navigeerimine", + "Toggle Sidebar": "Lülita külgriba", + "Toolbar Mode": "Tööriistariba režiim", + "Tools": "Tööriistad", + "Tree": "Puu", + "Trial Mode": "Proovirežiim", + "True": "Tõene", + "Try again": "Proovi uuesti", + "Typeahead Field": "Ennustava tekstiga väli", + "UI Mode": "Kasutajaliidese režiim", + "Unable to change password": "Parooli muutmine ebaõnnestus", + "Unable to delete filter preset": "Filtri eelseade kustutamine ebaõnnestus", + "Unable to delete view": "Vaate kustutamine ebaõnnestus", + "Unable to rename view": "Vaate ümbernimetamine ebaõnnestus", + "Unable to save changes": "Muudatuste salvestamine ebaõnnestus", + "Unable to save column preferences.": "Veergude eelistuste salvestamine ebaõnnestus.", + "Unable to save favorite": "Lemmiku salvestamine ebaõnnestus", + "Unable to save filter preset": "Filtri eelseade salvestamine ebaõnnestus", + "Unable to save role": "Rolli salvestamine ebaõnnestus", + "Unable to save view": "Vaate salvestamine ebaõnnestus", + "Unable to update filter preset": "Filtri eelseade uuendamine ebaõnnestus", + "Unauthorized": "Volitamata", + "Uncheck All": "Eemalda kõigilt märge", + "Underline": "Allakriipsutus", + "Unlink": "Eemalda link", + "Unlink All": "Eemalda kõik lingid", + "Unlisted Addons": "Loetlemata lisad", + "Unordered List": "Järjestamata nimekiri", + "Unpin from Favorites": "Eemalda lemmikutest", + "Unpublish": "Peida", + "Unpublish Entry|Unpublish :count Entries": "Peida kirje|Peida :count kirjet", + "Unpublished": "Peidetud", + "Unsaved changes": "Salvestamata muudatused", + "Up to date": "Ajakohane", + "Update": "Uuenda", + "Update All": "Uuenda kõik", + "Update successful.": "Uuendamine õnnestus.", + "Update to :version": "Uuenda versioonile :version", + "Update to Latest": "Uuenda uusimale", + "Updater": "Uuendaja", + "Updates": "Uuendused", + "Upload": "Laadi üles", + "Upload failed. The file is larger than is allowed by your server.": "Üleslaadimine ebaõnnestus. Fail on suurem, kui serveris lubatud.", + "Upload failed. The file might be larger than is allowed by your server.": "Üleslaadimine ebaõnnestus. Fail võib olla suurem, kui serveris lubatud.", + "Upload file": "Laadi fail üles", + "URL": "URL", + "Useful Links": "Kasulikud lingid", + "User": "Kasutaja", + "User created": "Kasutaja loodud", + "User deleted": "Kasutaja kustutatud", + "User group deleted": "Kasutajagrupp kustutatud", + "User group saved": "Kasutajagrupp salvestatud", + "User Groups": "Kasutajagrupid", + "User Information": "Kasutaja info", + "User saved": "Kasutaja salvestatud", + "Username": "Kasutajanimi", + "Users": "Kasutajad", + "Utilities": "Tööriistad", + "Validation": "Valideerimine", + "Validation Rules": "Valideerimisreeglid", + "Value": "Väärtus", + "View": "Vaade", + "View additional releases": "Vaata täiendavaid väljalaskeid", + "View All": "Vaata kõiki", + "View deleted": "Vaade kustutatud", + "View History": "Vaata ajalugu", + "View on Marketplace": "Vaata Marketplace'is", + "View renamed": "Vaade ümber nimetatud", + "View saved": "Vaade salvestatud", + "View Site": "Vaata saiti", + "View Useful Links": "Vaata kasulikke linke", + "Views created successfully": "Vaated edukalt loodud", + "Visibility": "Nähtavus", + "Visible": "Nähtav", + "Visit URL": "Külasta URL-i", + "Warm": "Soojenda", + "Warm Specific Presets": "Soojenda kindlaid eelseadeid", + "Warning! Changing a site handle may break existing site content!": "Hoiatus! Saidi pideme muutmine võib olemasoleva sisu lõhkuda!", + "Whoops!": "Oih!", + "Widgets": "Vidinad", + "Words": "Sõnad", + "Working Copy": "Töökoopia", + "Working copy has unsaved changes": "Töökoopial on salvestamata muudatusi", + "Write": "Kirjuta", + "You are not authorized to configure navs.": "Sul pole õigust navigeerimismenüüsid seadistada.", + "You are not authorized to create collections.": "Sul pole õigust kogumikke luua.", + "You are not authorized to create forms.": "Sul pole õigust vorme luua.", + "You are not authorized to create navs.": "Sul pole õigust navigeerimismenüüsid luua.", + "You are not authorized to create taxonomies.": "Sul pole õigust taksonoomiaid luua.", + "You are not authorized to delete navs.": "Sul pole õigust navigeerimismenüüsid kustutada.", + "You are not authorized to delete this collection.": "Sul pole õigust seda kogumikku kustutada.", + "You are not authorized to delete this taxonomy.": "Sul pole õigust seda taksonoomiat kustutada.", + "You are not authorized to edit this collection.": "Sul pole õigust seda kogumikku muuta.", + "You are not authorized to edit this taxonomy.": "Sul pole õigust seda taksonoomiat muuta.", + "You are not authorized to run this action.": "Sul pole õigust seda toimingut käivitada.", + "You are not authorized to scaffold resources.": "Sul pole õigust ressursse genereerida.", + "You are not authorized to view collections.": "Sul pole õigust kogumikke vaadata.", + "You are not authorized to view navs.": "Sul pole õigust navigeerimismenüüsid vaadata.", + "You are not authorized to view this collection.": "Sul pole õigust seda kogumikku vaadata.", + "You are now impersonating": "Sa esined nüüd teisena", + "You can't do this while logged in": "Seda ei saa sisselogituna teha", + "You're already editing this item.": "Sa juba redigeerid seda üksust.", + "Your Favorites": "Sinu lemmikud", + "Your working copy will be replaced by the contents of this revision.": "Sinu töökoopia asendatakse selle redaktsiooni sisuga." +} diff --git a/resources/lang/et/dictionary-countries.php b/resources/lang/et/dictionary-countries.php new file mode 100644 index 00000000000..2d0b4689b60 --- /dev/null +++ b/resources/lang/et/dictionary-countries.php @@ -0,0 +1,283 @@ + 'Aafrika', + 'regions.americas' => 'Ameerika', + 'regions.asia' => 'Aasia', + 'regions.europe' => 'Euroopa', + 'regions.oceania' => 'Okeaania', + 'regions.polar' => 'Polaaralad', + 'subregions.australia_new_zealand' => 'Austraalia ja Uus-Meremaa', + 'subregions.caribbean' => 'Kariibi mere piirkond', + 'subregions.central_america' => 'Kesk-Ameerika', + 'subregions.central_asia' => 'Kesk-Aasia', + 'subregions.eastern_africa' => 'Ida-Aafrika', + 'subregions.eastern_asia' => 'Ida-Aasia', + 'subregions.eastern_europe' => 'Ida-Euroopa', + 'subregions.melanesia' => 'Melaneesia', + 'subregions.micronesia' => 'Mikroneesia', + 'subregions.middle_africa' => 'Kesk-Aafrika', + 'subregions.northern_africa' => 'Põhja-Aafrika', + 'subregions.northern_america' => 'Põhja-Ameerika', + 'subregions.northern_europe' => 'Põhja-Euroopa', + 'subregions.polynesia' => 'Polüneesia', + 'subregions.southern_africa' => 'Lõuna-Aafrika', + 'subregions.southern_asia' => 'Lõuna-Aasia', + 'subregions.southern_europe' => 'Lõuna-Euroopa', + 'subregions.south_america' => 'Lõuna-Ameerika', + 'subregions.south_eastern_africa' => 'Kagu-Aafrika', + 'subregions.south_eastern_asia' => 'Kagu-Aasia', + 'subregions.western_africa' => 'Lääne-Aafrika', + 'subregions.western_asia' => 'Lääne-Aasia', + 'subregions.western_europe' => 'Lääne-Euroopa', + 'names.AFG' => 'Afganistan', + 'names.ALA' => 'Ahvenamaa', + 'names.ALB' => 'Albaania', + 'names.DZA' => 'Alžeeria', + 'names.ASM' => 'Ameerika Samoa', + 'names.AND' => 'Andorra', + 'names.AGO' => 'Angola', + 'names.AIA' => 'Anguilla', + 'names.ATA' => 'Antarktika', + 'names.ATG' => 'Antigua ja Barbuda', + 'names.ARG' => 'Argentina', + 'names.ARM' => 'Armeenia', + 'names.ABW' => 'Aruba', + 'names.AUS' => 'Austraalia', + 'names.AUT' => 'Austria', + 'names.AZE' => 'Aserbaidžaan', + 'names.BHS' => 'Bahama', + 'names.BHR' => 'Bahrein', + 'names.BGD' => 'Bangladesh', + 'names.BRB' => 'Barbados', + 'names.BLR' => 'Valgevene', + 'names.BEL' => 'Belgia', + 'names.BLZ' => 'Belize', + 'names.BEN' => 'Benin', + 'names.BMU' => 'Bermuda', + 'names.BTN' => 'Bhutan', + 'names.BOL' => 'Boliivia', + 'names.BES' => 'Bonaire, Sint Eustatius ja Saba', + 'names.BIH' => 'Bosnia ja Hertsegoviina', + 'names.BWA' => 'Botswana', + 'names.BVT' => 'Bouveta saar', + 'names.BRA' => 'Brasiilia', + 'names.IOT' => 'Briti India ookeani ala', + 'names.BRN' => 'Brunei', + 'names.BGR' => 'Bulgaaria', + 'names.BFA' => 'Burkina Faso', + 'names.BDI' => 'Burundi', + 'names.KHM' => 'Kambodža', + 'names.CMR' => 'Kamerun', + 'names.CAN' => 'Kanada', + 'names.CPV' => 'Cabo Verde', + 'names.CYM' => 'Kaimanisaared', + 'names.CAF' => 'Kesk-Aafrika Vabariik', + 'names.TCD' => 'Tšaad', + 'names.CHL' => 'Tšiili', + 'names.CHN' => 'Hiina', + 'names.CXR' => 'Jõulusaar', + 'names.CCK' => 'Kookossaared (Keelingi saared)', + 'names.COL' => 'Colombia', + 'names.COM' => 'Komoorid', + 'names.COG' => 'Kongo Vabariik', + 'names.COK' => 'Cooki saared', + 'names.CRI' => 'Costa Rica', + 'names.CIV' => 'Elevandiluurannik', + 'names.HRV' => 'Horvaatia', + 'names.CUB' => 'Kuuba', + 'names.CUW' => 'Curaçao', + 'names.CYP' => 'Küpros', + 'names.CZE' => 'Tšehhi', + 'names.COD' => 'Kongo Demokraatlik Vabariik', + 'names.DNK' => 'Taani', + 'names.DJI' => 'Djibouti', + 'names.DMA' => 'Dominica', + 'names.DOM' => 'Dominikaani Vabariik', + 'names.TLS' => 'Ida-Timor', + 'names.ECU' => 'Ecuador', + 'names.EGY' => 'Egiptus', + 'names.SLV' => 'El Salvador', + 'names.GNQ' => 'Ekvatoriaal-Guinea', + 'names.ERI' => 'Eritrea', + 'names.EST' => 'Eesti', + 'names.ETH' => 'Etioopia', + 'names.FLK' => 'Falklandi saared', + 'names.FRO' => 'Fääri saared', + 'names.FJI' => 'Fidži', + 'names.FIN' => 'Soome', + 'names.FRA' => 'Prantsusmaa', + 'names.GUF' => 'Prantsuse Guajaana', + 'names.PYF' => 'Prantsuse Polüneesia', + 'names.ATF' => 'Prantsuse Lõunaalad', + 'names.GAB' => 'Gabon', + 'names.GEO' => 'Gruusia', + 'names.GMB' => 'Gambia', + 'names.DEU' => 'Saksamaa', + 'names.GHA' => 'Ghana', + 'names.GIB' => 'Gibraltar', + 'names.GRC' => 'Kreeka', + 'names.GRL' => 'Gröönimaa', + 'names.GRD' => 'Grenada', + 'names.GLP' => 'Guadeloupe', + 'names.GUM' => 'Guam', + 'names.GTM' => 'Guatemala', + 'names.GGY' => 'Guernsey', + 'names.GIN' => 'Guinea', + 'names.GNB' => 'Guinea-Bissau', + 'names.GUY' => 'Guyana', + 'names.HTI' => 'Haiti', + 'names.HMD' => 'Heardi ja McDonaldi saared', + 'names.HND' => 'Honduras', + 'names.HKG' => 'Hongkong', + 'names.HUN' => 'Ungari', + 'names.ISL' => 'Island', + 'names.IND' => 'India', + 'names.IDN' => 'Indoneesia', + 'names.IRN' => 'Iraan', + 'names.IRQ' => 'Iraak', + 'names.IRL' => 'Iirimaa', + 'names.ISR' => 'Iisrael', + 'names.ITA' => 'Itaalia', + 'names.JAM' => 'Jamaica', + 'names.JPN' => 'Jaapan', + 'names.JEY' => 'Jersey', + 'names.JOR' => 'Jordaania', + 'names.KAZ' => 'Kasahstan', + 'names.KEN' => 'Keenia', + 'names.KIR' => 'Kiribati', + 'names.XKX' => 'Kosovo', + 'names.KWT' => 'Kuveit', + 'names.KGZ' => 'Kõrgõzstan', + 'names.LAO' => 'Laos', + 'names.LVA' => 'Läti', + 'names.LBN' => 'Liibanon', + 'names.LSO' => 'Lesotho', + 'names.LBR' => 'Libeeria', + 'names.LBY' => 'Liibüa', + 'names.LIE' => 'Liechtenstein', + 'names.LTU' => 'Leedu', + 'names.LUX' => 'Luksemburg', + 'names.MAC' => 'Macau', + 'names.MKD' => 'Põhja-Makedoonia', + 'names.MDG' => 'Madagaskar', + 'names.MWI' => 'Malawi', + 'names.MYS' => 'Malaisia', + 'names.MDV' => 'Maldiivid', + 'names.MLI' => 'Mali', + 'names.MLT' => 'Malta', + 'names.IMN' => 'Mani saar', + 'names.MHL' => 'Marshalli Saared', + 'names.MTQ' => 'Martinique', + 'names.MRT' => 'Mauritaania', + 'names.MUS' => 'Mauritius', + 'names.MYT' => 'Mayotte', + 'names.MEX' => 'Mehhiko', + 'names.FSM' => 'Mikroneesia Liiduriigid', + 'names.MDA' => 'Moldova', + 'names.MCO' => 'Monaco', + 'names.MNG' => 'Mongoolia', + 'names.MNE' => 'Montenegro', + 'names.MSR' => 'Montserrat', + 'names.MAR' => 'Maroko', + 'names.MOZ' => 'Mosambiik', + 'names.MMR' => 'Myanmar', + 'names.NAM' => 'Namiibia', + 'names.NRU' => 'Nauru', + 'names.NPL' => 'Nepal', + 'names.NLD' => 'Holland', + 'names.NCL' => 'Uus-Kaledoonia', + 'names.NZL' => 'Uus-Meremaa', + 'names.NIC' => 'Nicaragua', + 'names.NER' => 'Niger', + 'names.NGA' => 'Nigeeria', + 'names.NIU' => 'Niue', + 'names.NFK' => 'Norfolki saar', + 'names.PRK' => 'Põhja-Korea', + 'names.MNP' => 'Põhja-Mariaanid', + 'names.NOR' => 'Norra', + 'names.OMN' => 'Omaan', + 'names.PAK' => 'Pakistan', + 'names.PLW' => 'Belau', + 'names.PSE' => 'Palestiina', + 'names.PAN' => 'Panama', + 'names.PNG' => 'Paapua Uus-Guinea', + 'names.PRY' => 'Paraguay', + 'names.PER' => 'Peruu', + 'names.PHL' => 'Filipiinid', + 'names.PCN' => 'Pitcairni saared', + 'names.POL' => 'Poola', + 'names.PRT' => 'Portugal', + 'names.PRI' => 'Puerto Rico', + 'names.QAT' => 'Katar', + 'names.REU' => 'Réunion', + 'names.ROU' => 'Rumeenia', + 'names.RUS' => 'Venemaa', + 'names.RWA' => 'Rwanda', + 'names.SHN' => 'Saint Helena', + 'names.KNA' => 'Saint Kitts ja Nevis', + 'names.LCA' => 'Saint Lucia', + 'names.SPM' => 'Saint-Pierre ja Miquelon', + 'names.VCT' => 'Saint Vincent ja Grenadiinid', + 'names.BLM' => 'Saint-Barthélemy', + 'names.MAF' => 'Saint-Martin (Prantsusmaa osa)', + 'names.WSM' => 'Samoa', + 'names.SMR' => 'San Marino', + 'names.STP' => 'São Tomé ja Príncipe', + 'names.SAU' => 'Saudi Araabia', + 'names.SEN' => 'Senegal', + 'names.SRB' => 'Serbia', + 'names.SYC' => 'Seišellid', + 'names.SLE' => 'Sierra Leone', + 'names.SGP' => 'Singapur', + 'names.SXM' => 'Sint Maarten (Hollandi osa)', + 'names.SVK' => 'Slovakkia', + 'names.SVN' => 'Sloveenia', + 'names.SLB' => 'Saalomoni Saared', + 'names.SOM' => 'Somaalia', + 'names.ZAF' => 'Lõuna-Aafrika Vabariik', + 'names.SGS' => 'Lõuna-Georgia ja Lõuna-Sandwichi saared', + 'names.KOR' => 'Lõuna-Korea', + 'names.SSD' => 'Lõuna-Sudaan', + 'names.ESP' => 'Hispaania', + 'names.LKA' => 'Sri Lanka', + 'names.SDN' => 'Sudaan', + 'names.SUR' => 'Suriname', + 'names.SJM' => 'Svalbard ja Jan Mayen', + 'names.SWZ' => 'Svaasimaa', + 'names.SWE' => 'Rootsi', + 'names.CHE' => 'Šveits', + 'names.SYR' => 'Süüria', + 'names.TWN' => 'Taiwan', + 'names.TJK' => 'Tadžikistan', + 'names.TZA' => 'Tansaania', + 'names.THA' => 'Tai', + 'names.TGO' => 'Togo', + 'names.TKL' => 'Tokelau', + 'names.TON' => 'Tonga', + 'names.TTO' => 'Trinidad ja Tobago', + 'names.TUN' => 'Tuneesia', + 'names.TUR' => 'Türgi', + 'names.TKM' => 'Türkmenistan', + 'names.TCA' => 'Turksi ja Caicose saared', + 'names.TUV' => 'Tuvalu', + 'names.UGA' => 'Uganda', + 'names.UKR' => 'Ukraina', + 'names.ARE' => 'Araabia Ühendemiraadid', + 'names.GBR' => 'Ühendkuningriik', + 'names.USA' => 'Ameerika Ühendriigid', + 'names.UMI' => 'USA hajasaared', + 'names.URY' => 'Uruguay', + 'names.UZB' => 'Usbekistan', + 'names.VUT' => 'Vanuatu', + 'names.VAT' => 'Vatikan', + 'names.VEN' => 'Venezuela', + 'names.VNM' => 'Vietnam', + 'names.VGB' => 'Briti Neitsisaared', + 'names.VIR' => 'USA Neitsisaared', + 'names.WLF' => 'Wallis ja Futuna', + 'names.ESH' => 'Lääne-Sahara', + 'names.YEM' => 'Jeemen', + 'names.ZMB' => 'Sambia', + 'names.ZWE' => 'Zimbabwe', +]; diff --git a/resources/lang/et/dictionary-currencies.php b/resources/lang/et/dictionary-currencies.php new file mode 100644 index 00000000000..f09208520cc --- /dev/null +++ b/resources/lang/et/dictionary-currencies.php @@ -0,0 +1,123 @@ + 'Araabia Ühendemiraatide dirhem', + 'AFN' => 'Afganistani afgaani', + 'ALL' => 'Albaania lekk', + 'AMD' => 'Armeenia dramm', + 'ARS' => 'Argentina peeso', + 'AUD' => 'Austraalia dollar', + 'AZN' => 'Aserbaidžaani manat', + 'BAM' => 'Bosnia ja Hertsegoviina konverteeritav mark', + 'BDT' => 'Bangladeshi taka', + 'BGN' => 'Bulgaaria leev', + 'BHD' => 'Bahreini dinaar', + 'BIF' => 'Burundi frank', + 'BND' => 'Brunei dollar', + 'BOB' => 'Boliivia boliviaano', + 'BRL' => 'Brasiilia reaal', + 'BWP' => 'Botswana pula', + 'BYN' => 'Valgevene rubla', + 'BZD' => 'Belize\'i dollar', + 'CAD' => 'Kanada dollar', + 'CDF' => 'Kongo frank', + 'CHF' => 'Šveitsi frank', + 'CLP' => 'Tšiili peeso', + 'CNY' => 'Hiina jüaan', + 'COP' => 'Colombia peeso', + 'CRC' => 'Costa Rica koloon', + 'CVE' => 'Cabo Verde eskuudo', + 'CZK' => 'Tšehhi kroon', + 'DJF' => 'Djibouti frank', + 'DKK' => 'Taani kroon', + 'DOP' => 'Dominikaani peeso', + 'DZD' => 'Alžeeria dinaar', + 'EEK' => 'Eesti kroon', + 'EGP' => 'Egiptuse nael', + 'ERN' => 'Eritrea nakfa', + 'ETB' => 'Etioopia birr', + 'EUR' => 'Euro', + 'GBP' => 'Suurbritannia naelsterling', + 'GEL' => 'Gruusia laari', + 'GHS' => 'Ghana cedi', + 'GNF' => 'Guinea frank', + 'GTQ' => 'Guatemala ketsaal', + 'HKD' => 'Hongkongi dollar', + 'HNL' => 'Hondurase lempiira', + 'HRK' => 'Horvaatia kuna', + 'HUF' => 'Ungari forint', + 'IDR' => 'Indoneesia ruupia', + 'ILS' => 'Iisraeli uus seekel', + 'INR' => 'India ruupia', + 'IQD' => 'Iraagi dinaar', + 'IRR' => 'Iraani riaal', + 'ISK' => 'Islandi kroon', + 'JMD' => 'Jamaica dollar', + 'JOD' => 'Jordaania dinaar', + 'JPY' => 'Jaapani jeen', + 'KES' => 'Keenia šilling', + 'KHR' => 'Kambodža riaal', + 'KMF' => 'Komoori frank', + 'KRW' => 'Lõuna-Korea vonn', + 'KWD' => 'Kuveidi dinaar', + 'KZT' => 'Kasahstani tenge', + 'LBP' => 'Liibanoni nael', + 'LKR' => 'Sri Lanka ruupia', + 'LTL' => 'Leedu litt', + 'LVL' => 'Läti latt', + 'LYD' => 'Liibüa dinaar', + 'MAD' => 'Maroko dirhem', + 'MDL' => 'Moldova leu', + 'MGA' => 'Madagaskari ariaari', + 'MKD' => 'Makedoonia denaar', + 'MMK' => 'Myanmari kjatt', + 'MOP' => 'Macau pataaka', + 'MUR' => 'Mauritiuse ruupia', + 'MXN' => 'Mehhiko peeso', + 'MYR' => 'Malaisia ringgit', + 'MZN' => 'Mosambiigi metikal', + 'NAD' => 'Namiibia dollar', + 'NGN' => 'Nigeeria naira', + 'NIO' => 'Nicaragua kordoba', + 'NOK' => 'Norra kroon', + 'NPR' => 'Nepali ruupia', + 'NZD' => 'Uus-Meremaa dollar', + 'OMR' => 'Omaani riaal', + 'PAB' => 'Panama balboa', + 'PEN' => 'Peruu soll', + 'PHP' => 'Filipiinide peeso', + 'PKR' => 'Pakistani ruupia', + 'PLN' => 'Poola zlott', + 'PYG' => 'Paraguay guaranii', + 'QAR' => 'Katari riaal', + 'RON' => 'Rumeenia leu', + 'RSD' => 'Serbia dinaar', + 'RUB' => 'Venemaa rubla', + 'RWF' => 'Rwanda frank', + 'SAR' => 'Saudi Araabia riaal', + 'SDG' => 'Sudaani nael', + 'SEK' => 'Rootsi kroon', + 'SGD' => 'Singapuri dollar', + 'SOS' => 'Somaalia šilling', + 'SYP' => 'Süüria nael', + 'THB' => 'Tai baat', + 'TND' => 'Tuneesia dinaar', + 'TOP' => 'Tonga paʻanga', + 'TRY' => 'Türgi liir', + 'TTD' => 'Trinidadi ja Tobago dollar', + 'TWD' => 'Uus Taiwani dollar', + 'TZS' => 'Tansaania šilling', + 'UAH' => 'Ukraina grivna', + 'UGX' => 'Uganda šilling', + 'USD' => 'USA dollar', + 'UYU' => 'Uruguay peeso', + 'UZS' => 'Usbekistani somm', + 'VEF' => 'Venezuela boliivar', + 'VND' => 'Vietnami dong', + 'XAF' => 'CFA frank BEAC', + 'XOF' => 'CFA frank BCEAO', + 'YER' => 'Jeemeni riaal', + 'ZAR' => 'Lõuna-Aafrika rand', + 'ZMK' => 'Sambia kwacha', + 'ZWL' => 'Zimbabwe dollar', +]; diff --git a/resources/lang/et/fieldtypes.php b/resources/lang/et/fieldtypes.php new file mode 100644 index 00000000000..e5b6aed4c00 --- /dev/null +++ b/resources/lang/et/fieldtypes.php @@ -0,0 +1,208 @@ + 'Luba selle välja sisus Antlersi parsimist.', + 'any.config.cast_booleans' => 'Valikud väärtustega "true" ja "false" salvestatakse tõeväärtustena.', + 'any.config.mode' => 'Vali oma eelistatud kasutajaliidese stiil.', + 'array.config.expand' => 'Kas salvestada massiiv laiendatud formaadis? Kasuta seda, kui plaanid kasutada numbrilisi väärtusi.', + 'array.config.keys' => 'Määra massiivi võtmed (muutujad) ja valikulised sildid.', + 'array.config.mode' => 'Dünaamiline režiim annab kasutajale vaba kontrolli andmete üle, samas kui võtmega ja üksikrežiimid jõustavad ranged võtmed.', + 'array.title' => 'Massiiv', + 'asset_folders.config.container' => 'Vali selle välja jaoks kasutatav varakonteiner.', + 'assets.config.allow_uploads' => 'Luba uute failide üleslaadimist.', + 'assets.config.container' => 'Vali selle välja jaoks kasutatav varakonteiner.', + 'assets.config.dynamic' => 'Varad paigutatakse alamkausta, mis põhineb selle välja väärtusel.', + 'assets.config.folder' => 'Kaust, millest sirvimist alustada.', + 'assets.config.max_files' => 'Valitavate varade maksimaalne arv.', + 'assets.config.min_files' => 'Valitavate varade minimaalne arv.', + 'assets.config.mode' => 'Vali oma eelistatud paigutuse stiil.', + 'assets.config.query_scopes' => 'Vali, milliseid päringuulatusi tuleks valitavate varade toomisel rakendada.', + 'assets.config.restrict' => 'Takista kasutajatel teistesse kaustadesse navigeerimist.', + 'assets.config.show_filename' => 'Kuva failinimi eelvaatepildi kõrval.', + 'assets.config.show_set_alt' => 'Kuva link piltidele alternatiivteksti määramiseks.', + 'assets.dynamic_folder_pending_field' => 'See väli on saadaval, kui :field on määratud.', + 'assets.dynamic_folder_pending_save' => 'See väli on pärast salvestamist saadaval.', + 'assets.title' => 'Varad', + 'bard.config.allow_source' => 'Luba kirjutamise ajal HTML lähtekoodi vaatamist.', + 'bard.config.always_show_set_button' => 'Luba, et nupp "Lisa komplekt" oleks alati nähtav.', + 'bard.config.buttons' => 'Vali, milliseid nuppe tööriistaribal kuvada.', + 'bard.config.container' => 'Vali selle välja jaoks kasutatav varakonteiner.', + 'bard.config.enable_input_rules' => 'Lubab sisu tippimisel Markdown-stiilis otseteid.', + 'bard.config.enable_paste_rules' => 'Lubab sisu kleepimisel Markdown-stiilis otseteid.', + 'bard.config.fullscreen' => 'Luba täisekraanirežiimi lülitit.', + 'bard.config.inline' => 'Keela plokielemendid, näiteks pealkirjad, pildid ja komplektid.', + 'bard.config.inline.break' => 'Lubatud reavahetustega', + 'bard.config.inline.disabled' => 'Keelatud', + 'bard.config.inline.enabled' => 'Lubatud ilma reavahetusteta', + 'bard.config.link_collections' => 'Nende kogumike kirjed on lingivalijas saadaval. Tühjaks jätmine muudab kõik kirjed kättesaadavaks.', + 'bard.config.link_noopener' => 'Määra `rel="noopener"` kõikidele linkidele.', + 'bard.config.link_noreferrer' => 'Määra `rel="noreferrer"` kõikidele linkidele.', + 'bard.config.previews' => 'Kuvatakse, kui komplektid on ahendatud.', + 'bard.config.reading_time' => 'Näita välja allosas hinnangulist lugemisaega.', + 'bard.config.remove_empty_nodes' => 'Vali, kuidas tühjade sõlmedega toime tulla.', + 'bard.config.save_html' => 'Salvesta HTML struktureeritud andmete asemel. See lihtsustab, kuid piirab malli märgistuse kontrolli.', + 'bard.config.section.editor.instructions' => 'Seadista redaktori välimust ja üldist käitumist.', + 'bard.config.section.links.instructions' => 'Seadista, kuidas linke selles Bardi eksemplaris käsitletakse.', + 'bard.config.section.sets.instructions' => 'Seadista väljade plokke, mida saab Bardi sisusse lisada.', + 'bard.config.select_across_sites' => 'Luba kirjete valimist teistelt saitidelt. See keelab ka lokaliseerimisvalikud esiotsas. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/fieldtypes/entries#select-across-sites).', + 'bard.config.smart_typography' => 'Teisenda levinud tekstimustrid õigeteks tüpograafilisteks märkideks.', + 'bard.config.target_blank' => 'Määra `target="_blank"` kõikidele linkidele.', + 'bard.config.toolbar_mode' => '**Fikseeritud** režiimis on tööriistariba kogu aeg nähtav, samas kui **hõljuv** režiim kuvatakse ainult teksti valimisel.', + 'bard.config.word_count' => 'Kuva sõnade arv välja allosas.', + 'bard.title' => 'Bard', + 'button_group.title' => 'Nupugrupp', + 'checkboxes.config.inline' => 'Kuva märkeruudud reas.', + 'checkboxes.config.options' => 'Määra massiivi võtmed ja nende valikulised sildid.', + 'checkboxes.title' => 'Märkeruudud', + 'code.config.indent_size' => 'Määra eelistatud taande suurus (tühikutes).', + 'code.config.indent_type' => 'Määra eelistatud taande tüüp.', + 'code.config.key_map' => 'Vali eelistatud kiirklahvide komplekt.', + 'code.config.mode' => 'Vali süntaksi esiletõstmise keel.', + 'code.config.mode_selectable' => 'Kas kasutaja saab režiimi muuta.', + 'code.config.rulers' => 'Seadista vertikaalsed joonlauad taandamise hõlbustamiseks.', + 'code.config.theme' => 'Vali oma eelistatud teema.', + 'code.title' => 'Kood', + 'collections.title' => 'Kogumikud', + 'color.config.allow_any' => 'Luba mis tahes värviväärtuse sisestamine valija või heksakoodi abil.', + 'color.config.default' => 'Vali vaikevärv.', + 'color.config.swatches' => 'Eelnevalt määratletud värvid, mida saab loendist valida.', + 'color.title' => 'Värv', + 'date.config.columns' => 'Kuva mitu kuud korraga ridades ja veergudes.', + 'date.config.earliest_date' => 'Määra varaseim valitav kuupäev.', + 'date.config.format' => 'Kuidas kuupäeva salvestada, kasutades [PHP kuupäevavormingut](https://www.php.net/manual/en/datetime.format.php).', + 'date.config.full_width' => 'Venita kalender täislaiusesse.', + 'date.config.inline' => 'Jäta rippmenüü sisestusväli vahele ja kuva kalender otse.', + 'date.config.latest_date' => 'Määra hiliseim valitav kuupäev.', + 'date.config.mode' => 'Vali üksik- või vahemikurežiimi vahel (vahemiku režiimis keelatakse ajavalija).', + 'date.config.rows' => 'Kuva mitu kuud korraga ridades ja veergudes.', + 'date.config.time_enabled' => 'Luba ajavalija.', + 'date.config.time_seconds_enabled' => 'Näita sekundeid ajavalijas.', + 'date.title' => 'Kuupäev', + 'dictionary.config.dictionary' => 'Sõnastik, millest soovid valikuid hankida.', + 'dictionary.file.config.filename' => 'Sinu valikuid sisaldav failinimi, mis on seotud kataloogiga `resources/dictionaries`.', + 'dictionary.file.config.label' => 'Võti, mis sisaldab valikute silte. Vaikimisi on see `label`. Alternatiivina võid kasutada Antlersit.', + 'dictionary.file.config.value' => 'Võti, mis sisaldab valikute väärtusi. Vaikimisi on see `value`.', + 'entries.config.collections' => 'Vali, milliste kogumike hulgast kasutaja valida saab.', + 'entries.config.create' => 'Luba uute kirjete loomist.', + 'entries.config.query_scopes' => 'Vali, milliseid päringuulatusi tuleks valitavate kirjete toomisel rakendada.', + 'entries.config.search_index' => 'Võimaluse korral kasutatakse automaatselt sobivat otsinguindeksit, kuid võid määrata ka selgesõnalise indeksi.', + 'entries.config.select_across_sites' => 'Luba kirjete valimist teistelt saitidelt. See keelab ka lokaliseerimisvalikud esiotsas. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/fieldtypes/entries#select-across-sites).', + 'entries.title' => 'Kirjed', + 'float.title' => 'Ujukomaarv', + 'form.config.max_items' => 'Määra valitavate vormide maksimaalne arv.', + 'form.config.query_scopes' => 'Vali, milliseid päringuulatusi tuleks valitavate vormide toomisel rakendada.', + 'form.title' => 'Vorm', + 'grid.config.add_row' => 'Kohanda nupu "Lisa rida" silti.', + 'grid.config.border' => 'Kuva selle grupi väljade ümber ääris ja polsterdus.', + 'grid.config.fields' => 'Iga väli muutub ruudustiku tabelis veeruks.', + 'grid.config.fullscreen' => 'Luba täisekraanirežiimi lülitit.', + 'grid.config.max_rows' => 'Määra loodavate ridade maksimaalne arv.', + 'grid.config.min_rows' => 'Määra loodavate ridade minimaalne arv.', + 'grid.config.mode' => 'Vali oma eelistatud paigutuse stiil.', + 'grid.config.reorderable' => 'Luba ridade ümberjärjestamist.', + 'grid.title' => 'Ruudustik', + 'group.config.fields' => 'Seadista väljad, mis pesastatakse sellesse gruppi.', + 'group.title' => 'Grupp', + 'hidden.title' => 'Peidetud', + 'html.config.html_instruct' => 'Halda avaldamisvormil kuvatavat HTML-koodi.', + 'html.title' => 'HTML', + 'icon.config.directory' => 'Ikoone sisaldava kataloogi tee.', + 'icon.config.folder' => 'Alamkataloog, mis sisaldab kindlat ikoonikomplekti.', + 'icon.title' => 'Ikoon', + 'integer.title' => 'Täisarv', + 'link.config.collections' => 'Nende kogumike kirjed on saadaval. Tühjaks jätmine teeb kättesaadavaks marsruuditavate kogumike kirjed.', + 'link.config.container' => 'Vali selle välja jaoks kasutatav varakonteiner.', + 'link.title' => 'Link', + 'list.title' => 'Nimekiri', + 'markdown.config.automatic_line_breaks' => 'Luba automaatsed reavahetused.', + 'markdown.config.automatic_links' => 'Luba mis tahes URL-ide automaatset linkimist.', + 'markdown.config.container' => 'Vali selle välja jaoks kasutatav varakonteiner.', + 'markdown.config.escape_markup' => 'Eirab reasisest HTML-märgistust (nt `
` muutub `
`).', + 'markdown.config.folder' => 'Kaust, millest sirvimist alustada.', + 'markdown.config.heading_anchors' => 'Sisesta ankrulingid kõikidesse oma pealkirjaelementidesse (`

`, `

` jne).', + 'markdown.config.parser' => 'Kohandatud Markdowni parseri nimi. Vaikimisi valiku korral jäta tühjaks.', + 'markdown.config.restrict' => 'Takista kasutajatel teistesse kaustadesse navigeerimist.', + 'markdown.config.smartypants' => 'Teisenda sirged jutumärgid automaatselt looksulgu jutumärkideks, kriipsud en/em-kriipsudeks ja tee muid sarnaseid tekstiteisendusi.', + 'markdown.config.table_of_contents' => 'Lisa automaatselt sisukord koos linkidega pealkirjadele.', + 'markdown.title' => 'Markdown', + 'picker.category.controls.description' => 'Väljad, mis pakuvad valitavaid suvandeid või nuppe, mis saavad loogikat juhtida.', + 'picker.category.media.description' => 'Väljad, mis salvestavad pilte, videoid või muud meediat.', + 'picker.category.number.description' => 'Väljad, mis salvestavad numbreid.', + 'picker.category.relationship.description' => 'Väljad, mis salvestavad seoseid teiste ressurssidega.', + 'picker.category.special.description' => 'Need väljad on erilised, igaüks omal moel.', + 'picker.category.structured.description' => 'Väljad, mis salvestavad struktureeritud andmeid. Mõned saavad isegi teisi välju enda sisse pesastada.', + 'picker.category.text.description' => 'Väljad, mis salvestavad tekstistringe, rikastatud sisu või mõlemat.', + 'radio.config.inline' => 'Näita raadionuppe reas.', + 'radio.config.options' => 'Määra massiivi võtmed ja nende valikulised sildid.', + 'radio.title' => 'Raadio', + 'range.config.append' => 'Lisa tekst liuguri lõppu (paremale poole).', + 'range.config.max' => 'Maksimaalne, parempoolne väärtus.', + 'range.config.min' => 'Minimaalne, vasakpoolne väärtus.', + 'range.config.prepend' => 'Lisa tekst liuguri algusesse (vasakule poole).', + 'range.config.step' => 'Väärtuste vaheline minimaalne samm.', + 'range.title' => 'Vahemik', + 'relationship.config.mode' => 'Vali oma eelistatud kasutajaliidese stiil.', + 'replicator.config.button_label' => 'Silt nupule "Lisa komplekt".', + 'replicator.config.collapse' => 'Komplekti ahendamise käitumine.', + 'replicator.config.collapse.accordion' => 'Luba korraga laiendada ainult ühte komplekti.', + 'replicator.config.collapse.disabled' => 'Kõik komplektid on vaikimisi laiendatud.', + 'replicator.config.collapse.enabled' => 'Kõik komplektid on vaikimisi ahendatud.', + 'replicator.config.fullscreen' => 'Luba täisekraanirežiimi lülitit.', + 'replicator.config.max_sets' => 'Määra komplektide maksimaalne arv.', + 'replicator.config.previews' => 'Kuva ahendatud olekus komplekti sisu eelvaadet.', + 'replicator.config.sets' => 'Komplektid on seadistatavad väljade plokid, mida saab vastavalt soovile luua ja ümber järjestada.', + 'replicator.title' => 'Replikaator', + 'revealer.config.input_label' => 'Määra nupul või lüliti kõrval kuvatav silt.', + 'revealer.config.mode' => 'Vali oma eelistatud kasutajaliidese stiil.', + 'revealer.title' => 'Näitaja', + 'section.title' => 'Jaotis', + 'select.config.clearable' => 'Luba valiku tühistamist.', + 'select.config.multiple' => 'Luba mitut valikut.', + 'select.config.options' => 'Määra võtmed ja nende valikulised sildid.', + 'select.config.placeholder' => 'Määra kohatäite tekst.', + 'select.config.push_tags' => 'Lisa äsja loodud sildid valikute loendisse.', + 'select.config.searchable' => 'Luba võimalike valikute otsimist.', + 'select.config.taggable' => 'Luba lisaks eelnevalt määratletud valikutele uute valikute lisamist.', + 'select.title' => 'Valik', + 'sites.title' => 'Saidid', + 'slug.config.from' => 'Sihtväli, millest rööpurl luua.', + 'slug.config.generate' => 'Loo sihtmärgi `from` väljalt automaatselt rööpurl.', + 'slug.config.show_regenerate' => 'Kuva taasloomise nupp, et sihtväljalt rööpurl uuesti luua.', + 'slug.title' => 'Rööpurl', + 'structures.title' => 'Struktuurid', + 'table.config.max_columns' => 'Määra maksimaalne veergude arv.', + 'table.config.max_rows' => 'Määra ridade maksimaalne arv.', + 'table.title' => 'Tabel', + 'taggable.config.options' => 'Esita eelmääratletud sildid, mida saab valida.', + 'taggable.config.placeholder' => 'Sisesta ja vajuta ↩ Enter', + 'taggable.title' => 'Sildistatav', + 'taxonomies.title' => 'Taksonoomiad', + 'template.config.blueprint' => 'Lisab valiku "Infer from Blueprint". Lisateavet leiad [dokumentatsioonist](https://statamic.dev/views#inferring-templates-from-entry-blueprints).', + 'template.config.folder' => 'Kuva ainult selles kaustas olevad mallid.', + 'template.config.hide_partials' => 'Osalised vaated on harva mõeldud mallidena kasutamiseks.', + 'template.title' => 'Mall', + 'terms.config.create' => 'Luba uute terminite loomist.', + 'terms.config.query_scopes' => 'Vali, milliseid päringuulatusi tuleks valitavate terminite toomisel rakendada.', + 'terms.config.taxonomies' => 'Vali, millistest taksonoomiatest termineid kuvada.', + 'terms.title' => 'Taksonoomia terminid', + 'text.config.append' => 'Lisa tekst sisendi järele (paremale poole).', + 'text.config.autocomplete' => 'Määra automaatse täitmise atribuut.', + 'text.config.character_limit' => 'Määra sisestatavate märkide maksimaalne arv.', + 'text.config.input_type' => 'Määra HTML5 sisestustüüp.', + 'text.config.placeholder' => 'Määra kohatäite tekst.', + 'text.config.prepend' => 'Lisa tekst sisendi ette (vasakule).', + 'text.title' => 'Tekst', + 'textarea.title' => 'Tekstiväli', + 'time.config.seconds_enabled' => 'Näita sekundeid ajavalijas.', + 'time.title' => 'Aeg', + 'toggle.config.inline_label' => 'Määra lüliti sisendi kõrval kuvatav tekstisisene silt.', + 'toggle.config.inline_label_when_true' => 'Määra tekstisisene silt, mis kuvatakse, kui lüliti väärtus on tõene.', + 'toggle.title' => 'Lüliti', + 'user_groups.title' => 'Kasutajagrupid', + 'user_roles.title' => 'Kasutajarollid', + 'users.config.query_scopes' => 'Vali, milliseid päringuulatusi tuleks valitavate kasutajate toomisel rakendada.', + 'users.title' => 'Kasutajad', + 'video.title' => 'Video', + 'width.config.options' => 'Määra saadaolevad laiuse valikud.', + 'yaml.title' => 'YAML', +]; diff --git a/resources/lang/et/markdown.php b/resources/lang/et/markdown.php new file mode 100644 index 00000000000..8104298abbd --- /dev/null +++ b/resources/lang/et/markdown.php @@ -0,0 +1,57 @@ + ' +

Markdown on tekstist HTML-iks teisendamise süntaks veebikirjutajatele. Markdown võimaldab sul kirjutada kergesti loetavas ja kirjutatavas lihttekstivormingus, mis teisendatakse struktuurselt kehtivaks HTML-iks.

+ +

Pealkirjad

+
# See on H1
+## See on H2
+### See on H3 jne.
+
+ +

Rasvane & Kursiiv

+
Saad muuta teksti *kursiivi*, **rasvaseks** või _**mõlemat korraga**_.
+ +

Lingid

+
See on [näidislink](http://example.com).
+ +

Kood

+

+Ümbritse oma kood kolme tagurpidi ülakomaga (```) nii enne kui ka pärast koodiplokki. +

+ +
```
+see: on natuke yaml-i
+```
+ +

Saad lisada koodi ka reasiseselt, ümbritsedes sisu ühe tagurpidi ülakomaga `.

+ +

Tsitaat

+ +

Loo plokktsitaat, alustades oma teksti sümboliga > .

+ +
> See on plokktsitaat.
+ +

Pildid

+
![alternatiivtekst](http://example.com/image.jpg)
+ +

Järjestamata nimekiri

+
- Peekon
+- Steik
+- Õlu
+ +

Järjestatud nimekiri

+
1. Söö
+2. Joo
+3. Ole rõõmus
+ +

Tabelid

+ +
Esimene päis  | Teine päis
+------------- | -------------
+Lahtri sisu   | Lahtri sisu
+Lahtri sisu   | Lahtri sisu
', + +]; diff --git a/resources/lang/et/messages.php b/resources/lang/et/messages.php new file mode 100644 index 00000000000..48b590bd980 --- /dev/null +++ b/resources/lang/et/messages.php @@ -0,0 +1,258 @@ + 'Saite selle e-kirja, kuna saime sinu konto parooli lähtestamise taotluse.', + 'activate_account_notification_subject' => 'Aktiveeri oma konto', + 'addon_has_more_releases_beyond_license_body' => 'Saad uuendada, kuid pead selleks litsentsi uuendama või uue ostma.', + 'addon_has_more_releases_beyond_license_heading' => 'Sellel lisal on rohkem väljalaskeid, kui sinu litsents lubab.', + 'addon_install_command' => 'Selle lisa installimiseks käivita järgmine käsk', + 'addon_list_loading_error' => 'Lisade laadimisel tekkis viga. Proovi hiljem uuesti.', + 'addon_uninstall_command' => 'Selle lisa eemaldamiseks käivita järgmine käsk', + 'asset_container_allow_uploads_instructions' => 'Kui see on lubatud, saab kasutaja sellesse konteinerisse faile üles laadida.', + 'asset_container_blueprint_instructions' => 'Mallid määratlevad varade redigeerimisel saadaolevad täiendavad kohandatud väljad.', + 'asset_container_create_folder_instructions' => 'Kui see on lubatud, saavad kasutajad selles konteineris kaustu luua.', + 'asset_container_disk_instructions' => 'Failisüsteemi kettad määravad failide salvestuskoha – kas lokaalselt või kaugsalvestuses, näiteks Amazon S3-s. Neid saab seadistada failis `config/filesystems.php`.', + 'asset_container_handle_instructions' => 'Kasutatakse sellele konteinerile viitamiseks esiotsas. Selle hilisem muutmine võib lehe lõhkuda.', + 'asset_container_intro' => 'Meedia- ja dokumendifailid asuvad serveris või muudes failisalvestusteenustes kaustades. Iga sellist asukohta nimetatakse konteineriks.', + 'asset_container_move_instructions' => 'Kui see on lubatud, saavad kasutajad selles konteineris faile teisaldada.', + 'asset_container_quick_download_instructions' => 'Kui see on lubatud, lisatakse varahaldurisse kiire allalaadimise nupp.', + 'asset_container_rename_instructions' => 'Kui see on lubatud, saavad kasutajad selles konteineris olevaid faile ümber nimetada.', + 'asset_container_source_preset_instructions' => 'Üleslaaditud pilte töödeldakse selle eelseadistuse abil jäädavalt.', + 'asset_container_title_instructions' => 'Tavaliselt mitmuses nimisõna, näiteks "Pildid" või "Dokumendid".', + 'asset_container_validation_rules_instructions' => 'Neid reegleid rakendatakse üleslaaditud failidele.', + 'asset_container_warm_intelligent_instructions' => 'Genereeri üleslaadimisel sobivad eelseadistused.', + 'asset_container_warm_presets_instructions' => 'Määra, millised eelseadistused üleslaadimisel genereerida.', + 'asset_folders_directory_instructions' => 'URL-ide puhtuse tagamiseks soovitame vältida tühikuid ja erimärke.', + 'asset_replace_confirmation' => 'Selle vara viited sisus värskendatakse allpool valitud varaks.', + 'asset_reupload_confirmation' => 'Oled kindel, et soovid selle vara uuesti üles laadida?', + 'asset_reupload_warning' => 'Sul võib esineda brauseri või serveri tasemel vahemällu salvestamise probleeme. Võimalik, et eelistad hoopis vara asendada.', + 'blueprints_hidden_instructions' => 'Peidab malli juhtpaneeli loomise nuppude eest.', + 'blueprints_intro' => 'Mallid defineerivad ja korraldavad välju, et luua sisumudeleid kogumike, vormide ja muude andmetüüpide jaoks.', + 'blueprints_title_instructions' => 'Tavaliselt ainsuses nimisõna, näiteks "Artikkel" või "Toode".', + 'cache_utility_application_cache_description' => 'Laraveli ühtne vahemälu, mida kasutavad Statamic, kolmandate osapoolte lisad ja Composer\'i paketid.', + 'cache_utility_description' => 'Halda ja vaata olulist teavet Statamicu erinevate vahemälukihtide kohta.', + 'cache_utility_image_cache_description' => 'Piltide vahemälu salvestab kõigi teisendatud ja muudetud suurusega piltide koopiad.', + 'cache_utility_stache_description' => 'Stache on Statamicu sisusalvesti, mis toimib sarnaselt andmebaasiga. See genereeritakse sisufailidest automaatselt.', + 'cache_utility_static_cache_description' => 'Staatilised lehed mööduvad täielikult Statamicust ja renderdatakse maksimaalse jõudluse tagamiseks otse serverist.', + 'choose_entry_localization_deletion_behavior' => 'Vali toiming, mida soovid lokaliseeritud kirjetega teha.', + 'collection_configure_date_behavior_private' => 'Privaatne – loenditest peidetud, URL-id 404', + 'collection_configure_date_behavior_public' => 'Avalik – alati nähtav', + 'collection_configure_date_behavior_unlisted' => 'Loendist väljas – loenditest peidetud, URL-id nähtavad', + 'collection_configure_dated_instructions' => 'Avaldamiskuupäevi saab kasutada sisu ajastamiseks ja aegumiseks.', + 'collection_configure_handle_instructions' => 'Kasutatakse sellele kogumikule viitamiseks esiotsas. Selle hilisem muutmine võib lehe lõhkuda.', + 'collection_configure_intro' => 'Kogumikud on konteinerid, mis sisaldavad kirjeid, mis võivad esindada artikleid, blogipostitusi, tooteid, sündmusi või mis tahes muud tüüpi sisu.', + 'collection_configure_layout_instructions' => 'Määra selle kogumiku vaikepaigutus. Kirjed saavad selle sätte tühistada paigutuse väljaga nimega "layout". Selle sätte muutmine on ebatavaline.', + 'collection_configure_origin_behavior_instructions' => 'Millist saiti tuleks kirje lokaliseerimisel lähtekohana kasutada?', + 'collection_configure_origin_behavior_option_active' => 'Kasuta muudetava kirje aktiivset saiti', + 'collection_configure_origin_behavior_option_root' => 'Kasuta saiti, kus kirje algselt loodi', + 'collection_configure_origin_behavior_option_select' => 'Lase kasutajal valida päritolukoht', + 'collection_configure_propagate_instructions' => 'Levita uued kirjed automaatselt kõikidele seadistatud saitidele.', + 'collection_configure_require_slugs_instructions' => 'Kas kirjetel peavad olema rööpurlid (slugs).', + 'collection_configure_template_instructions' => 'Määra selle kogumiku vaikemall. Kirjed saavad selle sätte tühistada malliväljaga.', + 'collection_configure_title_format_instructions' => 'Määra see, et selle kogumiku kirjete pealkirjad genereeritaks automaatselt. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/collections#titles).', + 'collection_configure_title_instructions' => 'Soovitame mitmusevormis nimisõna, näiteks "Artiklid" või "Tooted".', + 'collection_next_steps_blueprints_description' => 'Halda selle kogumiku jaoks saadaolevaid malle ja välju.', + 'collection_next_steps_configure_description' => 'Seadista URL-e ja marsruute, määratle malle, kuupäevade käitumist, järjestamist ja muid valikuid.', + 'collection_next_steps_create_entry_description' => 'Loo esimene kirje või lisa mõned kohatäitekirjed, see on sinu otsustada.', + 'collection_next_steps_scaffold_description' => 'Kogumiku nime põhjal saab kiiresti genereerida registri- ja detailivaateid.', + 'collection_revisions_instructions' => 'Luba selle kogumiku redaktsioonid.', + 'collection_scaffold_instructions' => 'Vali, millised tühjad vaated genereerida. Olemasolevaid faile üle ei kirjutata.', + 'collections_blueprint_instructions' => 'Selle kogumiku kirjed võivad kasutada ükskõik millist neist mallidest.', + 'collections_default_publish_state_instructions' => 'Sellesse kogumikku uute kirjete loomisel on avaldamise lüliti vaikeväärtuseks **tõene**, mitte **väär** (mustand).', + 'collections_future_date_behavior_instructions' => 'Kuidas tulevaste kuupäevadega kirjed peaksid käituma.', + 'collections_links_instructions' => 'Selle kogumiku kirjed võivad sisaldada linke (ümbersuunamisi) teistele kirjetele või URL-idele.', + 'collections_mount_instructions' => 'Vali kirje, millele see kogumik paigaldada. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/collections-and-entries#mounting).', + 'collections_orderable_instructions' => 'Luba käsitsi järjestamist lohistamise teel.', + 'collections_past_date_behavior_instructions' => 'Kuidas peaksid käituma varasema kuupäevaga kirjed.', + 'collections_preview_target_refresh_instructions' => 'Eelvaadet värskendatakse redigeerimise ajal automaatselt. Selle keelamisel kasutatakse postMessage\'i.', + 'collections_preview_targets_instructions' => 'Reaalajas eelvaates kuvatavad URL-id. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/live-preview#preview-targets).', + 'collections_route_instructions' => 'Marsruut kontrollib kirjete URL-i mustrit. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/collections#routing).', + 'collections_sort_direction_instructions' => 'Vaikimisi sortimissuund.', + 'collections_taxonomies_instructions' => 'Seo selle kogumiku kirjed taksonoomiatega. Väljad lisatakse avaldamisvormidele automaatselt.', + 'dictionaries_countries_emojis_instructions' => 'Kas siltidele tuleks lisada lipuemotikone.', + 'dictionaries_countries_region_instructions' => 'Valikuliselt filtreeri riike piirkonna järgi.', + 'duplicate_action_localizations_confirmation' => 'Oled kindel, et soovid seda toimingut käivitada? Ka lokaliseerimised dubleeritakse.', + 'duplicate_action_warning_localization' => 'See kirje on lokaliseerimine. Algupärane kirje dubleeritakse.', + 'duplicate_action_warning_localizations' => 'Üks või mitu valitud kirjet on lokaliseerimised. Sellistel juhtudel dubleeritakse hoopis päritolukirje.', + 'email_utility_configuration_description' => 'Meiliseaded on seadistatud failis :path', + 'email_utility_description' => 'Kontrolli e-posti seadeid ja saada test-e-kirju.', + 'entry_origin_instructions' => 'Uus lokaliseerimine pärib väärtused valitud saidi kirjelt.', + 'expect_root_instructions' => 'Käsitle puu esimest lehte kui "juurlehte" või "avalehte".', + 'field_conditions_always_save_instructions' => 'Salvesta alati välja väärtus, isegi kui väli on peidetud.', + 'field_conditions_field_instructions' => 'Saad sisestada mis tahes välja pideme. Rippmenüü valikud ei ole piiratud.', + 'field_conditions_instructions' => 'Millal seda välja kuvada või peita.', + 'field_desynced_from_origin' => 'Algupärasest väärtusest sünkroonimine eemaldatud. Klõpsa sünkroonimiseks ja algse väärtuse taastamiseks.', + 'field_synced_with_origin' => 'Sünkroniseeritud algallikaga. Sünkroonimise lõpetamiseks klõpsa väljal või muuda seda.', + 'field_validation_advanced_instructions' => 'Lisa sellele väljale täpsem valideerimine.', + 'field_validation_required_instructions' => 'Määra, kas see väli on kohustuslik või mitte.', + 'field_validation_sometimes_instructions' => 'Valideeri ainult siis, kui see väli on nähtav või esitatud.', + 'fields_blueprints_description' => 'Mallid määratlevad sisustruktuuride, näiteks kogumike, taksonoomiate, kasutajate ja vormide väljad.', + 'fields_default_instructions' => 'Väärtus, mis sisestatakse alati, kui see väli on avaldamisvormil tühi.', + 'fields_display_instructions' => 'Välja silt, mis kuvatakse juhtpaneelil.', + 'fields_duplicate_instructions' => 'Kas see väli tuleks üksuse dubleerimisel lisada.', + 'fields_fieldsets_description' => 'Väljakomplektid on paindlikud ja valikulised väljade rühmad, mis aitavad korraldada korduvkasutatavaid, eelkonfigureeritud välju.', + 'fields_handle_instructions' => 'Välja mallimuutuja. Väldi [reserveeritud sõnade](https://statamic.dev/tips/reserved-words#as-field-names) kasutamist pidemetena.', + 'fields_instructions_instructions' => 'Esita täiendavaid väljajuhiseid. Markdown-vorming on toetatud.', + 'fields_instructions_position_instructions' => 'Kuva juhised välja kohal või all.', + 'fields_listable_instructions' => 'Kontrolli kirje veeru nähtavust.', + 'fields_replicator_preview_instructions' => 'Kontrolli eelvaate nähtavust Replikaatori/Bardi komplektides.', + 'fields_sortable_instructions' => 'Määra, kas väli peaks loendivaadetes sorteeritav olema.', + 'fields_visibility_instructions' => 'Kontrolli väljade nähtavust avaldamisvormidel.', + 'fieldset_import_fieldset_instructions' => 'Imporditav väljakomplekt.', + 'fieldset_import_prefix_instructions' => 'Eesliide, mis tuleks igale väljale importimisel lisada. nt kangelane_', + 'fieldset_intro' => 'Väljakomplektid on mallide valikuline lisa, toimides korduvkasutatavate osalistena, mida saab mallide sees kasutada.', + 'fieldset_link_fields_prefix_instructions' => 'Iga lingitud väljakomplekti välja pide lisatakse selle ette. See on kasulik, kui soovid samu välju mitu korda importida.', + 'fieldsets_handle_instructions' => 'Kasutatakse sellele väljakomplektile mujal viitamiseks. Selle hilisem muutmine võib lehe lõhkuda.', + 'fieldsets_title_instructions' => 'Tavaliselt kirjeldab, millised väljad sees on, näiteks "Pildiplokk" või "Metaandmed".', + 'filters_view_already_exists' => 'Selle nimega vaade on juba olemas. Selle vaate loomine kirjutab olemasoleva üle.', + 'focal_point_instructions' => 'Fookuspunkti määramine võimaldab dünaamilist pildikärpimist, kus objekt jääb kaadrisse.', + 'focal_point_previews_are_examples' => 'Kärpimise eelvaated on ainult näidised.', + 'forgot_password_enter_email' => 'Sisesta oma e-posti aadress, et saaksime saata parooli lähtestamise lingi.', + 'form_configure_blueprint_instructions' => 'Vali olemasolevate mallide hulgast või loo uus.', + 'form_configure_email_attachments_instructions' => 'Lisa üleslaaditud varad sellele e-kirjale.', + 'form_configure_email_bcc_instructions' => 'Pimekoopia saaja(te) e-posti aadressid – komadega eraldatud.', + 'form_configure_email_cc_instructions' => 'Koopia saaja(te) e-posti aadressid - komadega eraldatud.', + 'form_configure_email_from_instructions' => 'Jäta tühjaks, et kasutada saidi vaikesätteid.', + 'form_configure_email_html_instructions' => 'Selle e-kirja HTML-versiooni vaade.', + 'form_configure_email_instructions' => 'Seadista e-kirjad, mis saadetakse uute vormide esitamisel.', + 'form_configure_email_markdown_instructions' => 'Renderda selle e-kirja HTML-versioon Markdowni abil.', + 'form_configure_email_reply_to_instructions' => 'Vastuse saaja aadress. Jäta tühjaks, et kasutada saatja aadressi.', + 'form_configure_email_subject_instructions' => 'E-kirja teemarida.', + 'form_configure_email_text_instructions' => 'Selle meili tekstiversiooni vaade.', + 'form_configure_email_to_instructions' => 'Saaja(te) e-posti aadress - komaga eraldatud.', + 'form_configure_handle_instructions' => 'Kasutatakse sellele vormile viitamiseks esiotsas. Selle hilisem muutmine võib lehe lõhkuda.', + 'form_configure_honeypot_instructions' => 'Meepoti välja nimi. Meepotid on nähtamatud väljad, mida kasutatakse rämpsposti vähendamiseks.', + 'form_configure_intro' => 'Vorme kasutatakse külastajatelt teabe kogumiseks ning sündmuste ja teadete saatmiseks uute esituste korral.', + 'form_configure_mailer_instructions' => 'Vali selle e-kirja saatmiseks meiliprogramm. Vaikimisi saatja kasutamiseks jäta tühjaks.', + 'form_configure_store_instructions' => 'Esituste salvestamise peatamiseks keela see. Sündmuste ja e-posti teadete saatmine jätkub.', + 'form_configure_title_instructions' => 'Tavaliselt üleskutse tegutsemisele, näiteks "Võta meiega ühendust".', + 'getting_started_widget_blueprints' => 'Mallid määratlevad kohandatud väljad, mida kasutatakse sisu loomiseks ja salvestamiseks.', + 'getting_started_widget_collections' => 'Kogumikud sisaldavad saidi erinevat tüüpi sisu.', + 'getting_started_widget_docs' => 'Õpi Statamicut tundma, mõistes selle võimalusi.', + 'getting_started_widget_header' => 'Statamicuga alustamine', + 'getting_started_widget_intro' => 'Uue Statamicu saidi loomise alustamiseks soovitame alustada nende sammudega.', + 'getting_started_widget_navigation' => 'Loo mitmetasandilisi linkide loendeid, mida saab kasutada navigeerimisribade, jaluste jms renderdamiseks.', + 'getting_started_widget_pro' => 'Statamic Pro lisab piiramatult kasutajakontosid, rolle, õigusi, Giti integratsiooni, redaktsioone, mitme saidi tuge ja palju muud!', + 'git_disabled' => 'Statamic Giti integratsioon on hetkel keelatud.', + 'git_nothing_to_commit' => 'Pole midagi edastada, sisuteed on puhtad!', + 'git_utility_description' => 'Halda Giti poolt jälgitavat sisu.', + 'global_search_open_using_slash' => 'Fokuseeri globaalne otsing klahvi / abil.', + 'global_set_config_intro' => 'Globaalsed komplektid haldavad kogu saidil saadaolevat sisu, näiteks ettevõtte andmeid, kontaktandmeid või esiotsa seadeid.', + 'global_set_no_fields_description' => 'Saad lisada mallile välju või käsitsi lisada muutujaid komplektile endale.', + 'globals_blueprint_instructions' => 'Juhib muutujate muutmisel kuvatavaid välju.', + 'globals_configure_handle_instructions' => 'Kasutatakse selle globaalse komplekti viitamiseks esiotsas. Selle hilisem muutmine võib lehe lõhkuda.', + 'globals_configure_intro' => 'Globaalne komplekt on muutujate rühm, mis on saadaval kõigil esilehtedel.', + 'globals_configure_title_instructions' => 'Soovitame nimisõna, mis esindab komplekti sisu, nt "Bränd" või "Ettevõte".', + 'impersonate_action_confirmation' => 'Sind logitakse sisse selle kasutajana. Saad oma kontole naasta avatari menüü kaudu.', + 'licensing_config_cached_warning' => 'Muudatusi sinu .env või konfiguratsioonifailides ei tuvastata enne vahemälu tühjendamist. Kui näed siin ootamatuid litsentsimistulemusi, võib see olla põhjus. Vahemälu taastamiseks võid kasutada käsku `php artisan config:cache`.', + 'licensing_error_invalid_domain' => 'Kehtetu domeen', + 'licensing_error_invalid_edition' => 'Litsents kehtib :edition väljaandele', + 'licensing_error_no_domains' => 'Domeene pole määratletud', + 'licensing_error_no_site_key' => 'Saidi litsentsivõtit pole', + 'licensing_error_outside_license_range' => 'Litsents kehtib versioonidele :start kuni :end', + 'licensing_error_unknown_site' => 'Tundmatu sait', + 'licensing_error_unlicensed' => 'Litsentseerimata', + 'licensing_incorrect_key_format_body' => 'Tundub, et sinu saidivõti pole õiges vormingus. Palun kontrolli võtit ja proovi uuesti. Saad oma saidivõtme oma konto alt statamic.com-ist alla laadida. See on tähtnumbriline ja 16 tähemärki pikk. Veendu, et sa ei kasutaks pärandlitsentsivõtit, mis on UUID.', + 'licensing_incorrect_key_format_heading' => 'Vale saidivõtme vorming', + 'licensing_production_alert' => 'See sait kasutab Statamic Pro\'d ja kommertslisasid. Palun osta vastavad litsentsid.', + 'licensing_production_alert_addons' => 'See sait kasutab kommertslisasid. Palun osta vastavad litsentsid.', + 'licensing_production_alert_renew_statamic' => 'Selle Statamic Pro versiooni kasutamiseks on vaja litsentsi uuendada.', + 'licensing_production_alert_statamic' => 'See sait kasutab Statamic Pro\'d. Palun osta litsents.', + 'licensing_sync_instructions' => 'Statamic.com-i andmeid sünkroonitakse kord tunnis. Sundsünkroonimine kuvab kõik tehtud muudatused.', + 'licensing_trial_mode_alert' => 'See sait kasutab Statamic Pro\'d ja kommertslisasid. Enne avaldamist osta kindlasti litsentsid. Aitäh!', + 'licensing_trial_mode_alert_addons' => 'See sait kasutab kommertslisasid. Enne avaldamist osta kindlasti litsentsid. Aitäh!', + 'licensing_trial_mode_alert_statamic' => 'See sait kasutab Statamic Pro\'d. Enne avaldamist osta kindlasti litsents. Aitäh!', + 'licensing_utility_description' => 'Vaata ja lahenda litsentsimise üksikasju.', + 'max_depth_instructions' => 'Määra maksimaalne arv tasemeid, mida leht võib pesastada. Piirangu puudumisel jäta tühjaks.', + 'max_items_instructions' => 'Määra valitavate üksuste maksimaalne arv.', + 'navigation_configure_blueprint_instructions' => 'Vali olemasolevate mallide hulgast või loo uus.', + 'navigation_configure_collections_instructions' => 'Luba linkimist nende kogumike kirjetele.', + 'navigation_configure_handle_instructions' => 'Kasutatakse selle navigeerimise viitamiseks esiotsas. Selle hilisem muutmine võib lehe lõhkuda.', + 'navigation_configure_intro' => 'Navigatsioonid on mitmetasandilised linkide loendid, mida saab kasutada navigeerimisribade, jaluste, saidikaartide ja muude esiotsa navigatsioonivormide loomiseks.', + 'navigation_configure_select_across_sites' => 'Luba teistelt saitidelt kirjete valimist.', + 'navigation_configure_settings_intro' => 'Luba kogumikega linkimine, määra maksimaalne sügavus ja muud toimingud.', + 'navigation_configure_title_instructions' => 'Soovitame nime, mis sobib seal, kus seda kasutatakse, näiteks "Peanavigatsioon" või "Jaluse navigatsioon".', + 'navigation_documentation_instructions' => 'Lisateave navigatsioonide loomise, seadistamise ja renderdamise kohta.', + 'navigation_link_to_entry_instructions' => 'Lisa kirjele link. Luba seadete alas linkimine täiendavatele kogumikele.', + 'navigation_link_to_url_instructions' => 'Lisa link mis tahes sise- või välis-URL-ile. Luba seadete alas linkimine kirjetele.', + 'outpost_error_422' => 'Viga statamic.com-iga suhtlemisel.', + 'outpost_error_429' => 'Liiga palju päringuid saidile statamic.com.', + 'outpost_issue_try_later' => 'Statamic.com-iga suhtlemisel tekkis probleem. Palun proovi hiljem uuesti.', + 'outpost_license_key_error' => 'Statamicul ei õnnestunud esitatud litsentsivõtme faili dekrüpteerida. Palun laadi see uuesti alla saidilt statamic.com.', + 'password_protect_enter_password' => 'Avamiseks sisesta parool', + 'password_protect_incorrect_password' => 'Vale parool.', + 'password_protect_token_invalid' => 'Kehtetu või aegunud token.', + 'password_protect_token_missing' => 'Turvatoken puudub. Pead sellele ekraanile jõudma algselt kaitstud URL-ilt.', + 'phpinfo_utility_description' => 'Kontrolli PHP seadeid ja installitud mooduleid.', + 'preference_favorites_instructions' => 'Otseteed, mis kuvatakse globaalse otsinguriba avamisel. Alternatiivina võid lehte külastada ja selle loendisse lisamiseks kasutada ülaosas olevat kinnitusikooni.', + 'preference_locale_instructions' => 'Juhtpaneeli eelistatud keel.', + 'preference_start_page_instructions' => 'Juhtpaneelile sisselogimisel kuvatav leht.', + 'publish_actions_create_revision' => 'Töökoopia põhjal luuakse redaktsioon. Praegune redaktsioon ei muutu.', + 'publish_actions_current_becomes_draft_because_scheduled' => 'Kuna praegune redaktsioon on avaldatud ja oled valinud tuleviku kuupäeva, siis pärast esitamist toimib redaktsioon valitud kuupäevani mustandina.', + 'publish_actions_publish' => 'Töökoopia muudatused rakendatakse kirjele ja see avaldatakse kohe.', + 'publish_actions_schedule' => 'Töökoopia muudatused rakendatakse kirjele ja see avaldatakse valitud kuupäeval.', + 'publish_actions_unpublish' => 'Praegune redaktsioon muudetakse mustandiks.', + 'reset_password_notification_body' => 'Saite selle e-kirja, kuna saime sinu konto parooli lähtestamise taotluse.', + 'reset_password_notification_no_action' => 'Kui sa parooli lähtestamist ei taotlenud, pole edasisi toiminguid vaja.', + 'reset_password_notification_subject' => 'Parooli lähtestamise teavitus', + 'role_change_handle_warning' => 'Pideme muutmine ei uuenda kasutajate ja gruppide viiteid sellele.', + 'role_handle_instructions' => 'Selle rolli viitamiseks esiotsas kasutatakse pidemeid. Neid ei saa kergesti muuta.', + 'role_intro' => 'Rollid on juurdepääsu- ja toiminguõiguste grupid, mida saab määrata kasutajatele ja kasutajagruppidele.', + 'role_title_instructions' => 'Tavaliselt ainsuses nimisõna, näiteks "Toimetaja" või "Administraator".', + 'search_utility_description' => 'Halda ja vaata olulist teavet Statamicu otsinguindeksite kohta.', + 'session_expiry_enter_password' => 'Jätkamiseks sisesta oma parool.', + 'session_expiry_logged_out_for_inactivity' => 'Sind logiti passiivsuse tõttu välja.', + 'session_expiry_logging_out_in_seconds' => 'Sind logitakse passiivsuse tõttu välja :seconds sekundi pärast. Klõpsa seansi pikendamiseks.', + 'session_expiry_new_window' => 'Avaneb uues aknas. Tule tagasi, kui oled sisse loginud.', + 'show_slugs_instructions' => 'Kas kuvada puuvaates rööpurleid.', + 'site_configure_attributes_instructions' => 'Lisa oma saidi seadetele suvalisi atribuute, millele pääseb ligi mallide kaudu. [Lisateave](https://statamic.dev/multi-site#additional-attributes).', + 'site_configure_handle_instructions' => 'Selle saidi unikaalne viide. Saab hiljem muuta, kuid see võib lehe lõhkuda.', + 'site_configure_lang_instructions' => 'Lisateavet [keelte] kohta (https://statamic.dev/multi-site#language).', + 'site_configure_locale_instructions' => 'Lisateavet [lokaalide] kohta (https://statamic.dev/multi-site#locale).', + 'site_configure_name_instructions' => 'Kasutajale nähtav nimi, mida kuvatakse kogu juhtpaneelil.', + 'site_configure_url_instructions' => 'Lisateavet [saidi URL-ide] kohta (https://statamic.dev/multi-site#url).', + 'status_expired_with_date' => 'Aegunud :date', + 'status_published_with_date' => 'Avaldatud :date', + 'status_scheduled_with_date' => 'Avaldatakse :date', + 'taxonomies_blueprints_instructions' => 'Selle taksonoomia terminid võivad kasutada ükskõik millist neist mallidest.', + 'taxonomies_collections_instructions' => 'Kogumikud, mis seda taksonoomiat kasutavad.', + 'taxonomies_preview_target_refresh_instructions' => 'Eelvaadet värskendatakse redigeerimise ajal automaatselt. Selle keelamisel kasutatakse postMessage\'i.', + 'taxonomies_preview_targets_instructions' => 'Reaalajas eelvaates kuvatavad URL-id. Lisateavet leiad [dokumentatsioonist](https://statamic.dev/live-preview#preview-targets).', + 'taxonomy_configure_handle_instructions' => 'Kasutatakse selle taksonoomia viitamiseks esiotsas. Selle hilisem muutmine võib lehe lõhkuda.', + 'taxonomy_configure_intro' => 'Taksonoomia on süsteem, mis liigitab andmeid unikaalsete tunnuste, näiteks kategooriate, siltide või värvide, järgi.', + 'taxonomy_configure_layout_instructions' => 'Määra selle taksonoomia vaikepaigutus. Terminid saavad selle sätte tühistada paigutusväljaga.', + 'taxonomy_configure_template_instructions' => 'Määra selle taksonoomia vaikemall.', + 'taxonomy_configure_term_template_instructions' => 'Määra selle taksonoomia terminite vaikemall. Terminid saavad selle sätte tühistada malliväljaga.', + 'taxonomy_configure_title_instructions' => 'Soovitame kasutada mitmusevormis nimisõna, näiteks "Kategooriad" või "Sildid".', + 'taxonomy_next_steps_blueprints_description' => 'Halda selle taksonoomia jaoks saadaolevaid malle ja välju.', + 'taxonomy_next_steps_configure_description' => 'Seadista nimesid, seo kogumikke, määratle malle ja palju muud.', + 'taxonomy_next_steps_create_term_description' => 'Loo esimene termin või lisa mõned kohatäited, see on sinu otsustada.', + 'try_again_in_seconds' => '{0,1}Proovi kohe uuesti.|Proovi uuesti :count sekundi pärast.', + 'units.B' => ':count B', + 'units.GB' => ':count GB', + 'units.KB' => ':count KB', + 'units.MB' => ':count MB', + 'units.ms' => ':countms', + 'units.s' => ':counts', + 'updater_require_version_command' => 'Konkreetse versiooni nõudmiseks käivita järgmine käsk', + 'updater_update_to_latest_command' => 'Uusimale versioonile värskendamiseks käivita järgmine käsk', + 'uploader_append_timestamp' => 'Lisa ajatempel', + 'uploader_choose_new_filename' => 'Vali uus failinimi', + 'uploader_discard_use_existing' => 'Hülga ja kasuta olemasolevat faili', + 'uploader_overwrite_existing' => 'Kirjuta olemasolev fail üle', + 'user_activation_email_not_sent_error' => 'Aktiveerimismeili ei õnnestunud saata. Palun kontrolli oma e-posti seadeid ja proovi uuesti.', + 'user_groups_intro' => 'Kasutajagrupid võimaldavad sul kasutajaid korraldada ja õigustepõhiseid rolle koondvormis rakendada.', + 'user_groups_role_instructions' => 'Määra rollid, et anda selle grupi kasutajatele kõik vastavad õigused.', + 'user_groups_title_instructions' => 'Tavaliselt mitmuses nimisõna, näiteks "Toimetajad" või "Fotograafid".', + 'user_wizard_account_created' => 'Kasutajakonto on loodud.', + 'user_wizard_intro' => 'Kasutajatele saab määrata rolle, mis kohandavad nende õigusi, juurdepääsu ja võimeid kogu juhtpaneelil.', + 'user_wizard_invitation_body' => 'Aktiveeri oma uus Statamicu konto saidil :site, et hakata seda veebisaiti haldama. Sinu turvalisuse huvides aegub allolev link :expiry tunni pärast. Pärast seda võta uue parooli saamiseks ühendust saidi administraatoriga.', + 'user_wizard_invitation_intro' => 'Saada uuele kasutajale tervitusmeil konto aktiveerimise andmetega.', + 'user_wizard_invitation_share' => 'Kopeeri need andmed ja jaga neid aadressil :email sulle sobival viisil.', + 'user_wizard_invitation_share_before' => 'Pärast kasutaja loomist antakse sulle andmed, mida saad jagada e-posti aadressil :email sinu eelistatud viisil.', + 'user_wizard_invitation_subject' => 'Aktiveeri oma uus Statamicu konto saidil :site', + 'user_wizard_roles_groups_intro' => 'Kasutajatele saab määrata rolle, mis kohandavad nende õigusi, juurdepääsu ja võimeid kogu juhtpaneelil.', + 'user_wizard_super_admin_instructions' => 'Superadministraatoritel on täielik kontroll ja juurdepääs kõigele juhtpaneelil. Anna see roll targalt.', + 'view_more_count' => 'Vaata veel :count', + 'width_x_height' => ':width × :height', +]; diff --git a/resources/lang/et/moment.php b/resources/lang/et/moment.php new file mode 100644 index 00000000000..ab1e4421cbf --- /dev/null +++ b/resources/lang/et/moment.php @@ -0,0 +1,18 @@ + '%s pärast', + 'relativeTime.past' => '%s tagasi', + 'relativeTime.s' => 'mõni sekund', + 'relativeTime.ss' => '%d sekundit', + 'relativeTime.m' => 'minut', + 'relativeTime.mm' => '%d minutit', + 'relativeTime.h' => 'tund', + 'relativeTime.hh' => '%d tundi', + 'relativeTime.d' => 'päev', + 'relativeTime.dd' => '%d päeva', + 'relativeTime.M' => 'kuu', + 'relativeTime.MM' => '%d kuud', + 'relativeTime.y' => 'aasta', + 'relativeTime.yy' => '%d aastat', +]; diff --git a/resources/lang/et/permissions.php b/resources/lang/et/permissions.php new file mode 100644 index 00000000000..ffb96530682 --- /dev/null +++ b/resources/lang/et/permissions.php @@ -0,0 +1,88 @@ + 'Superkasutaja', + 'super_desc' => 'Superadministraatoritel on täielik kontroll ja juurdepääs kõigele juhtpaneelil. Anna see roll targalt.', + 'group_cp' => 'Juhtpaneel', + 'access_cp' => 'Juurdepääs juhtpaneelile', + 'access_cp_desc' => 'Lubab juurdepääsu juhtpaneelile, kuid ei garanteeri, et pärast sisenemist saab seal midagi teha.', + 'configure_sites' => 'Seadista saite', + 'configure_sites_desc' => 'Võimalus seadistada saite, kui mitme saidi funktsioon on lubatud.', + 'configure_fields' => 'Seadista välju', + 'configure_fields_desc' => 'Võimalus muuta malle, väljakomplekte ja nende välju.', + 'configure_addons' => 'Seadista lisasid', + 'configure_addons_desc' => 'Juurdepääs lisade alale lisade installimiseks ja eemaldamiseks.', + 'manage_preferences' => 'Halda eelistusi', + 'manage_preferences_desc' => 'Võimalus kohandada globaalseid ja rollipõhiseid eelistusi.', + 'group_sites' => 'Saidid', + 'access_{site}_site' => 'Juurdepääs saidile :site', + 'group_collections' => 'Kogumikud', + 'configure_collections' => 'Seadista kogumikke', + 'configure_collections_desc' => 'Annab juurdepääsu kõigile kogumikega seotud õigustele.', + 'view_{collection}_entries' => 'Vaata kirjeid kogumikus :collection', + 'edit_{collection}_entries' => 'Muuda kirjeid', + 'create_{collection}_entries' => 'Loo uusi kirjeid', + 'delete_{collection}_entries' => 'Kustuta kirjeid', + 'publish_{collection}_entries' => 'Halda avaldamise staatust', + 'publish_{collection}_entries_desc' => 'Võimalus muuta mustandist avaldatuks ja vastupidi.', + 'reorder_{collection}_entries' => 'Järjesta kirjeid ümber', + 'reorder_{collection}_entries_desc' => 'Võimaldab lohistades ümberjärjestamist.', + 'edit_other_authors_{collection}_entries' => 'Muuda teiste autorite kirjeid', + 'publish_other_authors_{collection}_entries' => 'Halda teiste autorite kirjete avaldamise staatust', + 'delete_other_authors_{collection}_entries' => 'Kustuta teiste autorite kirjeid', + 'group_taxonomies' => 'Taksonoomiad', + 'configure_taxonomies' => 'Seadista taksonoomiaid', + 'configure_taxonomies_desc' => 'Annab juurdepääsu kõigile taksonoomiatega seotud õigustele.', + 'view_{taxonomy}_terms' => 'Vaata termineid taksonoomias :taxonomy', + 'edit_{taxonomy}_terms' => 'Muuda termineid', + 'create_{taxonomy}_terms' => 'Loo uusi termineid', + 'delete_{taxonomy}_terms' => 'Kustuta termineid', + 'publish_{taxonomy}_terms' => 'Halda avaldamise staatust', + 'reorder_{taxonomy}_terms' => 'Järjesta termineid ümber', + 'group_navigation' => 'Navigatsioon', + 'configure_navs' => 'Seadista navigeerimist', + 'configure_navs_desc' => 'Annab juurdepääsu kõigile navigeerimisega seotud õigustele.', + 'view_{nav}_nav' => 'Vaata navigeerimist :nav', + 'edit_{nav}_nav' => 'Muuda navigeerimist', + 'group_globals' => 'Globaalid', + 'configure_globals' => 'Seadista globaale', + 'configure_globals_desc' => 'Annab juurdepääsu kõigile globaalidega seotud õigustele.', + 'edit_{global}_globals' => 'Muuda globaale :global', + 'group_assets' => 'Varad', + 'configure_asset_containers' => 'Seadista varakonteinereid', + 'configure_asset_containers_desc' => 'Annab juurdepääsu kõigile varadega seotud õigustele.', + 'view_{container}_assets' => 'Vaata varasid konteineris :container', + 'upload_{container}_assets' => 'Laadi üles uusi varasid', + 'edit_{container}_assets' => 'Muuda varasid', + 'move_{container}_assets' => 'Teisalda varasid', + 'rename_{container}_assets' => 'Nimeta varasid ümber', + 'delete_{container}_assets' => 'Kustuta varasid', + 'group_forms' => 'Vormid', + 'configure_forms' => 'Seadista vorme', + 'configure_forms_desc' => 'Annab juurdepääsu kõigile vormidega seotud õigustele.', + 'configure_form_fields' => 'Seadista vormivälju', + 'configure_form_fields_desc' => 'Võimalus muuta vormide malle, väljakomplekte ja nende välju.', + 'view_{form}_form_submissions' => 'Vaata esitusi vormil :form', + 'delete_{form}_form_submissions' => 'Kustuta esitusi vormil :form', + 'group_users' => 'Kasutajad', + 'view_users' => 'Vaata kasutajaid', + 'edit_users' => 'Muuda kasutajaid', + 'create_users' => 'Loo kasutajaid', + 'delete_users' => 'Kustuta kasutajaid', + 'change_passwords' => 'Muuda paroole', + 'edit_user_groups' => 'Muuda gruppe', + 'edit_roles' => 'Muuda rolle', + 'assign_user_groups' => 'Määra kasutajatele gruppe', + 'assign_roles' => 'Määra kasutajatele rolle', + 'impersonate_users' => 'Esine teiste kasutajatena', + 'group_updates' => 'Uuendused', + 'view_updates' => 'Vaata uuendusi', + 'group_utilities' => 'Tööriistad', + 'access_utility' => ':title', + 'access_utility_desc' => 'Annab juurdepääsu tööriistale :title', + 'group_misc' => 'Muu', + 'resolve_duplicate_ids' => 'Lahenda duplikaat-ID-d', + 'resolve_duplicate_ids_desc' => 'Annab võimaluse näha ja lahendada duplikaat-ID-sid.', + 'view_graphql' => 'Vaata GraphQL', + 'view_graphql_desc' => 'Annab juurdepääsu GraphQL-i graafilisele kasutajaliidesele.', +]; diff --git a/resources/lang/et/validation.php b/resources/lang/et/validation.php new file mode 100644 index 00000000000..bb0077ed9b9 --- /dev/null +++ b/resources/lang/et/validation.php @@ -0,0 +1,156 @@ + 'See väli peab olema aktsepteeritud.', + 'accepted_if' => 'See väli peab olema aktsepteeritud, kui :other on :value.', + 'active_url' => 'See ei ole kehtiv URL.', + 'after' => 'See peab olema kuupäev pärast kuupäeva :date.', + 'after_or_equal' => 'See peab olema kuupäev, mis on hilisem või sama kui :date.', + 'alpha' => 'See väli tohib sisaldada ainult tähti.', + 'alpha_dash' => 'See väli tohib sisaldada ainult tähti, numbreid, kriipse ja alakriipse.', + 'alpha_num' => 'See väli tohib sisaldada ainult tähti ja numbreid.', + 'array' => 'See väli peab olema massiiv.', + 'ascii' => 'See väli tohib sisaldada ainult ühebaidiseid tähtnumbrilisi märke ja sümboleid.', + 'before' => 'See peab olema kuupäev enne kuupäeva :date.', + 'before_or_equal' => 'See peab olema kuupäev, mis on varasem või sama kui :date.', + 'between.array' => 'Sisu peab olema :min ja :max üksuse vahel.', + 'between.file' => 'Faili suurus peab olema :min ja :max kilobaidi vahel.', + 'between.numeric' => 'See väärtus peab olema :min ja :max vahel.', + 'between.string' => 'Sisu peab olema :min ja :max tähemärgi pikkune.', + 'boolean' => 'See väli peab olema tõene või väär.', + 'can' => 'See väli sisaldab volitamata väärtust.', + 'confirmed' => 'Kinnitus ei kattu.', + 'current_password' => 'Parool on vale.', + 'date' => 'See ei ole kehtiv kuupäev.', + 'date_equals' => 'See peab olema kuupäev, mis on sama kui :date.', + 'date_format' => 'See ei vasta formaadile :format.', + 'decimal' => 'See väli peab sisaldama :decimal komakohta.', + 'declined' => 'See väli tuleb tagasi lükata.', + 'declined_if' => 'See väli tuleb tagasi lükata, kui :other on :value.', + 'different' => 'See väli ja :other peavad olema erinevad.', + 'digits' => 'See väli peab olema :digits numbrit pikk.', + 'digits_between' => 'See väli peab olema :min ja :max numbri vahel.', + 'dimensions' => 'Pildil on valed mõõtmed.', + 'distinct' => 'Sellel väljal on duplikaatväärtus.', + 'doesnt_end_with' => 'See väli ei tohi lõppeda ühegagi järgmistest: :values.', + 'doesnt_start_with' => 'See väli ei tohi alata ühegagi järgmistest: :values.', + 'email' => 'See peab olema kehtiv e-posti aadress.', + 'ends_with' => 'See väli peab lõppema ühega järgmistest: :values.', + 'enum' => 'Valitud väärtus on kehtetu.', + 'exists' => 'Valitud väärtus on kehtetu.', + 'file' => 'See väli peab olema fail.', + 'filled' => 'Sellel väljal peab olema väärtus.', + 'gt.array' => 'See väli peab sisaldama rohkem kui :value üksust.', + 'gt.file' => 'Fail peab olema suurem kui :value kilobaiti.', + 'gt.numeric' => 'See väärtus peab olema suurem kui :value.', + 'gt.string' => 'See väli peab olema pikem kui :value tähemärki.', + 'gte.array' => 'See väli peab sisaldama vähemalt :value üksust.', + 'gte.file' => 'Fail peab olema vähemalt :value kilobaiti.', + 'gte.numeric' => 'See väärtus peab olema suurem või võrdne kui :value.', + 'gte.string' => 'See väli peab olema vähemalt :value tähemärki pikk.', + 'image' => 'See väli peab olema pilt.', + 'in' => 'Valitud väärtus on kehtetu.', + 'in_array' => 'Seda väärtust ei eksisteeri :other seas.', + 'integer' => 'See väli peab olema täisarv.', + 'ip' => 'See väli peab olema kehtiv IP-aadress.', + 'ipv4' => 'See väli peab olema kehtiv IPv4-aadress.', + 'ipv6' => 'See väli peab olema kehtiv IPv6-aadress.', + 'json' => 'See väli peab olema kehtiv JSON-string.', + 'lowercase' => 'See väli peab olema väiketähtedega.', + 'lt.array' => 'See väli peab sisaldama vähem kui :value üksust.', + 'lt.file' => 'Fail peab olema väiksem kui :value kilobaiti.', + 'lt.numeric' => 'See väärtus peab olema väiksem kui :value.', + 'lt.string' => 'See väli peab olema lühem kui :value tähemärki.', + 'lte.array' => 'See väli ei tohi sisaldada rohkem kui :value üksust.', + 'lte.file' => 'Fail ei tohi olla suurem kui :value kilobaiti.', + 'lte.numeric' => 'See väärtus peab olema väiksem või võrdne kui :value.', + 'lte.string' => 'See väli ei tohi olla pikem kui :value tähemärki.', + 'mac_address' => 'See väli peab olema kehtiv MAC-aadress.', + 'max.array' => 'See väli ei tohi sisaldada rohkem kui :max üksust.', + 'max.file' => 'Fail ei tohi olla suurem kui :max kilobaiti.', + 'max.numeric' => 'See väärtus ei tohi olla suurem kui :max.', + 'max.string' => 'See väli ei tohi olla pikem kui :max tähemärki.', + 'max_digits' => 'See väli ei tohi sisaldada rohkem kui :max numbrit.', + 'mimes' => 'Fail peab olema tüübiga: :values.', + 'mimetypes' => 'Fail peab olema tüübiga: :values.', + 'min.array' => 'See väli peab sisaldama vähemalt :min üksust.', + 'min.file' => 'Fail peab olema vähemalt :min kilobaiti.', + 'min.numeric' => 'See väärtus peab olema vähemalt :min.', + 'min.string' => 'See väli peab olema vähemalt :min tähemärki pikk.', + 'min_digits' => 'See väli peab sisaldama vähemalt :min numbrit.', + 'missing' => 'See väli peab puuduma.', + 'missing_if' => 'See väli peab puuduma, kui :other on :value.', + 'missing_unless' => 'See väli peab puuduma, v.a juhul, kui :other on :value.', + 'missing_with' => 'See väli peab puuduma, kui :values on olemas.', + 'missing_with_all' => 'See väli peab puuduma, kui :values on olemas.', + 'multiple_of' => 'See väärtus peab olema :value kordne.', + 'not_in' => 'Valitud väärtus on kehtetu.', + 'not_regex' => 'Selle välja formaat on kehtetu.', + 'numeric' => 'See väli peab olema number.', + 'present' => 'See väli peab olemas olema.', + 'prohibited' => 'See väli on keelatud.', + 'prohibited_if' => 'See väli on keelatud, kui :other on :value.', + 'prohibited_unless' => 'See väli on keelatud, v.a juhul, kui :other on :values.', + 'prohibits' => 'See väli keelab välja :other olemasolu.', + 'regex' => 'Selle välja formaat on kehtetu.', + 'required' => 'See väli on kohustuslik.', + 'required_array_keys' => 'See väli peab sisaldama kirjeid järgmistele: :values.', + 'required_if' => 'See väli on kohustuslik, kui :other on :value.', + 'required_if_accepted' => 'See väli on kohustuslik, kui :other on aktsepteeritud.', + 'required_unless' => 'See väli on kohustuslik, v.a juhul, kui :other on :values.', + 'required_with' => 'See väli on kohustuslik, kui :values on olemas.', + 'required_with_all' => 'See väli on kohustuslik, kui :values on olemas.', + 'required_without' => 'See väli on kohustuslik, kui :values ei ole olemas.', + 'required_without_all' => 'See väli on kohustuslik, kui ükski :values ei ole olemas.', + 'same' => 'See väli ja :other peavad kattuma.', + 'size.array' => 'See väli peab sisaldama :size üksust.', + 'size.file' => 'Fail peab olema :size kilobaiti.', + 'size.numeric' => 'See väärtus peab olema :size.', + 'size.string' => 'See väli peab olema :size tähemärki pikk.', + 'starts_with' => 'See väli peab algama ühega järgmistest: :values.', + 'string' => 'See väli peab olema tekst.', + 'timezone' => 'See väli peab olema kehtiv ajavöönd.', + 'ulid' => 'See väli peab olema kehtiv ULID.', + 'unique' => 'See väärtus on juba võetud.', + 'uploaded' => 'Selle välja üleslaadimine ebaõnnestus.', + 'uppercase' => 'See väli peab olema suurtähtedega.', + 'url' => 'See väli peab olema kehtiv URL.', + 'uuid' => 'See väli peab olema kehtiv UUID.', + 'arr_fieldtype' => 'See väärtus on kehtetu.', + 'handle' => 'Pide tohib sisaldada ainult väiketähti, numbreid ja alakriipse.', + 'handle_starts_with_number' => 'Pide ei tohi alata numbriga.', + 'slug' => 'Rööpurl tohib sisaldada ainult väiketähti, numbreid, kriipse ja alakriipse.', + 'code_fieldtype_rulers' => 'See väärtus on kehtetu.', + 'composer_package' => 'See peab olema kehtiv Composer\'i paketi nimi (nt hasselhoff/kung-fury).', + 'date_fieldtype_date_required' => 'Kuupäev on kohustuslik.', + 'date_fieldtype_end_date_invalid' => 'Lõppkuupäev on kehtetu.', + 'date_fieldtype_end_date_required' => 'Lõppkuupäev on kohustuslik.', + 'date_fieldtype_only_single_mode_allowed' => 'Režiimi "Üksik" saab kasutada ainult siis, kui välja pide on "date".', + 'date_fieldtype_start_date_invalid' => 'Alguskuupäev on kehtetu.', + 'date_fieldtype_start_date_required' => 'Alguskuupäev on kohustuslik.', + 'date_fieldtype_time_required' => 'Aeg on kohustuslik.', + 'duplicate_field_handle' => 'Väli pidemega :handle on juba olemas.', + 'duplicate_uri' => 'Duplikaat URI: :value', + 'email_available' => 'Selle e-posti aadressiga kasutaja on juba olemas.', + 'fieldset_imported_recursively' => 'Väljakomplekti :handle imporditakse rekursiivselt.', + 'one_site_without_origin' => 'Vähemalt ühel saidil ei tohi olla päritolu.', + 'options_require_keys' => 'Kõigil valikutel peavad olema võtmed.', + 'origin_cannot_be_disabled' => 'Keelatud päritolu ei saa valida.', + 'parent_cannot_be_itself' => 'Vanem ei saa olla tema ise.', + 'parent_causes_root_children' => 'See põhjustaks juurlehel alamelemente.', + 'parent_exceeds_max_depth' => 'See ületaks maksimaalse sügavuse.', + 'reserved' => 'See on reserveeritud sõna.', + 'reserved_field_handle' => 'Välja pide :handle on reserveeritud sõna.', + 'unique_entry_value' => 'See väärtus on juba võetud.', + 'unique_form_handle' => 'See pide on juba võetud.', + 'unique_term_value' => 'See väärtus on juba võetud.', + 'unique_user_value' => 'See väärtus on juba võetud.', + 'unique_uri' => 'See URI on juba võetud.', + 'time' => 'See ei ole kehtiv aeg.', + 'asset_current_filename' => 'See on praeguse faili nimi.', + 'asset_file_exists' => 'Selle nimega fail on juba olemas.', + 'asset_file_exists_same_content' => 'Sama nime ja sisuga fail on juba olemas. Võid selle ümbernimetamise asemel kustutada.', + 'asset_file_exists_different_content' => 'Sama nimega fail on juba olemas, kuid selle sisu on erinev. Võid teise faili sellega asendada.', + 'custom.attribute-name.rule-name' => 'kohandatud sõnum', + 'attributes' => [], +]; diff --git a/src/Preferences/CorePreferences.php b/src/Preferences/CorePreferences.php index 81f2b265fb2..bb42c71294f 100644 --- a/src/Preferences/CorePreferences.php +++ b/src/Preferences/CorePreferences.php @@ -61,6 +61,7 @@ private function localeOptions(): array 'de_CH' => 'German (Switzerland)', 'en' => 'English', 'es' => 'Spanish', + 'et' => 'Estonian', 'fa' => 'Persian', 'fr' => 'French', 'hu' => 'Hungarian', From 5bc591ec8a98957afc21e42b0b2c0f560274d45b Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Jun 2025 15:02:29 +0100 Subject: [PATCH 247/490] [5.x] Fix authorization error when creating globals (#11883) --- src/Policies/GlobalSetPolicy.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Policies/GlobalSetPolicy.php b/src/Policies/GlobalSetPolicy.php index 6f48eb36b49..cb0d1bb0630 100644 --- a/src/Policies/GlobalSetPolicy.php +++ b/src/Policies/GlobalSetPolicy.php @@ -36,6 +36,11 @@ public function create($user) // handled by before() } + public function store($user) + { + // handled by before() + } + public function view($user, $set) { $user = User::fromUser($user); From 7fd5d4e8cf9785bd3a30197e714f91b37fc16950 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Jun 2025 15:02:59 +0100 Subject: [PATCH 248/490] [5.x] Add `hasField` method to `Fieldset` (#11882) --- src/Fields/Fieldset.php | 5 +++++ tests/Fields/FieldsetTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/Fields/Fieldset.php b/src/Fields/Fieldset.php index 98bfbb16aa7..44e43ddd9e7 100644 --- a/src/Fields/Fieldset.php +++ b/src/Fields/Fieldset.php @@ -108,6 +108,11 @@ public function field(string $handle): ?Field return $this->fields()->get($handle); } + public function hasField($field) + { + return $this->fields()->has($field); + } + public function isNamespaced(): bool { return Str::contains($this->handle(), '::'); diff --git a/tests/Fields/FieldsetTest.php b/tests/Fields/FieldsetTest.php index 4fef1e73fc7..ff8fb006d08 100644 --- a/tests/Fields/FieldsetTest.php +++ b/tests/Fields/FieldsetTest.php @@ -167,6 +167,32 @@ public function gets_a_single_field() $this->assertNull($fieldset->field('unknown')); } + #[Test] + public function it_can_check_if_has_field() + { + FieldsetRepository::shouldReceive('find') + ->with('partial') + ->andReturn((new Fieldset)->setContents([ + 'fields' => [ + ['handle' => 'two', 'field' => ['type' => 'text']], + ], + ])) + ->once(); + + $fieldset = new Fieldset; + + $fieldset->setContents([ + 'fields' => [ + ['handle' => 'one', 'field' => ['type' => 'text']], + ['import' => 'partial'], + ], + ]); + + $this->assertTrue($fieldset->hasField('one')); + $this->assertTrue($fieldset->hasField('two')); + $this->assertFalse($fieldset->hasField('three')); + } + #[Test] public function gets_blueprints_importing_fieldset() { From ecaed56c2c86aa0a7b2df22d38e5e2db85f3b20e Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Thu, 19 Jun 2025 15:03:43 +0100 Subject: [PATCH 249/490] [5.x] Ensure propagating entries respects saveQuietly (#11875) --- src/Entries/Entry.php | 4 +- tests/Data/Entries/EntryTest.php | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index e83ed2d3b6f..f44c9e927ae 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -436,8 +436,8 @@ public function save() if ($isNew && ! $this->hasOrigin() && $this->collection()->propagate()) { $this->collection()->sites() ->reject($this->site()->handle()) - ->each(function ($siteHandle) { - $this->makeLocalization($siteHandle)->save(); + ->each(function ($siteHandle) use ($withEvents) { + $this->makeLocalization($siteHandle)->{$withEvents ? 'save' : 'saveQuietly'}(); }); } diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index d5f788782db..bffe08eef18 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -1507,6 +1507,69 @@ public function it_propagates_entry_if_configured() }); } + #[Test] + public function it_doesnt_fire_events_when_propagating_entry_and_saved_quietly() + { + Event::fake(); + + $this->setSites([ + 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], + 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], + 'es' => ['name' => 'Spanish', 'locale' => 'es_ES', 'url' => 'http://test.com/es/'], + 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], + ]); + + $collection = (new Collection) + ->handle('pages') + ->propagate(true) + ->sites(['en', 'fr', 'de']) + ->save(); + + $entry = (new Entry) + ->id('a') + ->locale('en') + ->collection($collection); + + $return = $entry->saveQuietly(); + + $this->assertIsObject($fr = $entry->descendants()->get('fr')); + $this->assertIsObject($de = $entry->descendants()->get('de')); + $this->assertNull($entry->descendants()->get('es')); // collection not configured for this site + + Event::assertDispatchedTimes(EntrySaving::class, 0); + Event::assertNotDispatched(EntrySaving::class, function ($event) use ($entry) { + return $event->entry === $entry; + }); + Event::assertNotDispatched(EntrySaving::class, function ($event) use ($fr) { + return $event->entry === $fr; + }); + Event::assertNotDispatched(EntrySaving::class, function ($event) use ($de) { + return $event->entry === $de; + }); + + Event::assertDispatchedTimes(EntryCreated::class, 0); + Event::assertNotDispatched(EntryCreated::class, function ($event) use ($entry) { + return $event->entry === $entry; + }); + Event::assertNotDispatched(EntryCreated::class, function ($event) use ($fr) { + return $event->entry === $fr; + }); + Event::assertNotDispatched(EntryCreated::class, function ($event) use ($de) { + return $event->entry === $de; + }); + + Event::assertDispatchedTimes(EntrySaved::class, 0); + Event::assertNotDispatched(EntrySaved::class, function ($event) use ($entry) { + return $event->entry === $entry; + }); + Event::assertNotDispatched(EntrySaved::class, function ($event) use ($fr) { + return $event->entry === $fr; + }); + Event::assertNotDispatched(EntrySaved::class, function ($event) use ($de) { + return $event->entry === $de; + }); + } + #[Test] public function it_propagates_entry_from_non_default_site_if_configured() { From 34577477d5374eed1c45ba9f7771371fe84925ad Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 19 Jun 2025 09:07:36 -0500 Subject: [PATCH 250/490] [5.x] Fix issues with Blade nav tag compiler (#11872) --- src/Providers/ViewServiceProvider.php | 22 ++++--- src/View/Blade/Concerns/CompilesNavs.php | 15 +++-- .../AntlersComponents/NavCompilerTest.php | 59 +++++++++++++++++++ 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/Providers/ViewServiceProvider.php b/src/Providers/ViewServiceProvider.php index f944749278a..d52cbc73628 100644 --- a/src/Providers/ViewServiceProvider.php +++ b/src/Providers/ViewServiceProvider.php @@ -5,7 +5,6 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\View as ViewFactory; use Illuminate\Support\ServiceProvider; -use Illuminate\Support\Str; use Illuminate\View\View; use Statamic\Contracts\View\Antlers\Parser as ParserContract; use Statamic\Facades\Site; @@ -178,18 +177,17 @@ public function registerBladeDirectives() $nested = '$children'; } - $recursiveChildren = <<<'PHP' -@include('compiled__views::'.$__currentStatamicNavView, array_merge(get_defined_vars(), [ - 'depth' => ($depth ?? 0) + 1, - '__statamicOverrideTagResultValue' => #varName#, -])) + return << (\$depth ?? 0) + 1, + '__statamicOverrideTagResultValue' => $nested, + ]), + \$___statamicNavCallback + ); +?> PHP; - - $recursiveChildren = Str::swap([ - '#varName#' => $nested, - ], $recursiveChildren); - - return Blade::compileString($recursiveChildren); }); } diff --git a/src/View/Blade/Concerns/CompilesNavs.php b/src/View/Blade/Concerns/CompilesNavs.php index 3d92ba3dfc7..c8549ad1327 100644 --- a/src/View/Blade/Concerns/CompilesNavs.php +++ b/src/View/Blade/Concerns/CompilesNavs.php @@ -12,13 +12,20 @@ protected function compileNav(ComponentNode $component): string $viewName = '___nav'.sha1($component->outerDocumentContent); $compiled = (new StatamicTagCompiler()) - ->prependCompiledContent('$__currentStatamicNavView = \''.$viewName.'\';') - ->appendCompiledContent('unset($__currentStatamicNavView);') ->setInterceptNav(false) ->compile($component->outerDocumentContent); - file_put_contents(storage_path('framework/views/'.$viewName.'.blade.php'), $compiled); + return <<$compiled +PHP; } } diff --git a/tests/View/Blade/AntlersComponents/NavCompilerTest.php b/tests/View/Blade/AntlersComponents/NavCompilerTest.php index 7ad021026ac..853ca6631eb 100644 --- a/tests/View/Blade/AntlersComponents/NavCompilerTest.php +++ b/tests/View/Blade/AntlersComponents/NavCompilerTest.php @@ -232,4 +232,63 @@ public function it_renders_aliased_recursive_children() Blade::render($template) ); } + + #[Test] + public function it_supports_imported_classes_and_functions() + { + $template = <<<'BLADE' +@use (Statamic\Support\Html) + +
    + +@foreach ($the_items as $item) +
  • {{ Html::entities($item['title']) }}
  • +@endforeach +
    +
+BLADE; + + $expected = <<<'EXPECTED' +
    +
  • Home
  • +
  • About
  • +
  • Projects
  • +
  • Contact
  • +
+EXPECTED; + + $this->assertSame( + $expected, + Blade::render($template), + ); + } + + #[Test] + public function it_doesnt_mangle_php_inside_nav_tag() + { + $template = <<<'BLADE' +
    + +@foreach ($the_items as $item) +@php $theValue = $item['title'].'-value'; @endphp +
  • {{ $theValue }}
  • +@endforeach +
    +
+BLADE; + + $expected = <<<'EXPECTED' +
    +
  • Home-value
  • +
  • About-value
  • +
  • Projects-value
  • +
  • Contact-value
  • +
+EXPECTED; + + $this->assertSame( + $expected, + Blade::render($template), + ); + } } From d4398baf2a8644e398710f25896c7e208c069858 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Thu, 19 Jun 2025 15:40:50 +0100 Subject: [PATCH 251/490] [5.x] Ensure nav blueprint graphql types are registered (#11881) --- src/GraphQL/TypeRegistrar.php | 2 ++ src/GraphQL/Types/NavPageInterface.php | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/GraphQL/TypeRegistrar.php b/src/GraphQL/TypeRegistrar.php index 9bebf1df91e..30936cc9834 100644 --- a/src/GraphQL/TypeRegistrar.php +++ b/src/GraphQL/TypeRegistrar.php @@ -17,6 +17,7 @@ use Statamic\GraphQL\Types\GlobalSetInterface; use Statamic\GraphQL\Types\JsonArgument; use Statamic\GraphQL\Types\LabeledValueType; +use Statamic\GraphQL\Types\NavPageInterface; use Statamic\GraphQL\Types\NavTreeBranchType; use Statamic\GraphQL\Types\NavType; use Statamic\GraphQL\Types\PageInterface; @@ -71,6 +72,7 @@ public function register() AssetInterface::addTypes(); GlobalSetInterface::addTypes(); UserType::addTypes(); + NavPageInterface::addTypes(); $this->registered = true; } diff --git a/src/GraphQL/Types/NavPageInterface.php b/src/GraphQL/Types/NavPageInterface.php index b6129e28dfa..b542fd61b15 100644 --- a/src/GraphQL/Types/NavPageInterface.php +++ b/src/GraphQL/Types/NavPageInterface.php @@ -5,6 +5,7 @@ use Rebing\GraphQL\Support\InterfaceType; use Statamic\Contracts\Structures\Nav; use Statamic\Facades\GraphQL; +use Statamic\Facades\Nav as NavAPI; use Statamic\Support\Str; class NavPageInterface extends InterfaceType @@ -34,4 +35,11 @@ public static function buildName(Nav $nav): string { return 'NavPage_'.Str::studly($nav->handle()); } + + public static function addTypes() + { + GraphQL::addTypes(NavAPI::all()->each(function ($nav) { + optional($nav->blueprint())->addGqlTypes(); + })->mapInto(NavBasicPageType::class)->all()); + } } From 98d90c4f9c3ca00086b0f562a56d5f4ec6e33bb4 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Thu, 19 Jun 2025 21:09:17 +0200 Subject: [PATCH 252/490] [5.x] Fix files not being removed after cache has been cleared (#11873) Co-authored-by: Jason Varga --- src/StaticCaching/Cachers/FileCacher.php | 36 +++++++++++++++++++++++- tests/StaticCaching/FileCacherTest.php | 27 ++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/StaticCaching/Cachers/FileCacher.php b/src/StaticCaching/Cachers/FileCacher.php index 78dc3531d21..f0816308f33 100644 --- a/src/StaticCaching/Cachers/FileCacher.php +++ b/src/StaticCaching/Cachers/FileCacher.php @@ -5,8 +5,10 @@ use Illuminate\Contracts\Cache\Repository; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\LazyCollection; use Statamic\Events\UrlInvalidated; use Statamic\Facades\File; +use Statamic\Facades\Path; use Statamic\Facades\Site; use Statamic\StaticCaching\Page; use Statamic\StaticCaching\Replacers\CsrfTokenReplacer; @@ -136,9 +138,41 @@ public function invalidateUrl($url, $domain = null) $this->forgetUrl($key, $domain); }); + $this->getFiles($site) + ->filter(fn ($file) => str_starts_with($file, $url.'_')) + ->each(function ($file, $path) { + $this->writer->delete($path); + }); + UrlInvalidated::dispatch($url, $domain); } + /** + * Get lazy collection file listing. + * + * @param Site $site + */ + private function getFiles($site): LazyCollection + { + $cachePath = $this->getCachePath($site); + if (! $cachePath || ! File::exists($cachePath)) { + return LazyCollection::make(); + } + + $directoryIterator = new \RecursiveDirectoryIterator($cachePath, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS); + $iterator = new \RecursiveIteratorIterator($directoryIterator); + + return LazyCollection::make(function () use ($iterator, $cachePath) { + foreach ($iterator as $file) { + if (! $file->isFile() || $file->getExtension() !== 'html') { + continue; + } + + yield Path::tidy($file->getPathName()) => Str::start(Str::replaceFirst($cachePath, '', $file->getPathName()), '/'); + } + }); + } + public function getCachePaths() { $paths = $this->config('path'); @@ -183,7 +217,7 @@ public function getFilePath($url, $site = null) $basename = $slug.'_lqs_'.md5($query).'.html'; } - return $this->getCachePath($site).$pathParts['dirname'].'/'.$basename; + return Path::tidy($this->getCachePath($site).Str::finish($pathParts['dirname'], '/').$basename); } private function isBasenameTooLong($basename) diff --git a/tests/StaticCaching/FileCacherTest.php b/tests/StaticCaching/FileCacherTest.php index bc522b5d5ae..fda08112c63 100644 --- a/tests/StaticCaching/FileCacherTest.php +++ b/tests/StaticCaching/FileCacherTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Events\UrlInvalidated; +use Statamic\Facades\File; use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\Cachers\FileCacher; use Statamic\StaticCaching\Cachers\Writer; @@ -297,6 +298,32 @@ public function invalidating_a_url_deletes_the_file_and_removes_the_url_when_usi $this->assertEquals(['one' => '/one'], $cacher->getUrls('http://domain.de')->all()); } + #[Test] + public function invalidating_a_url_deletes_the_file_even_if_it_is_not_in_application_cache() + { + $writer = \Mockery::spy(Writer::class); + $cache = app(Repository::class); + $cacher = $this->fileCacher([ + 'path' => public_path('static'), + ], $writer, $cache); + + File::put($cacher->getFilePath('/one'), ''); + File::put($cacher->getFilePath('/one?foo=bar'), ''); + File::put($cacher->getFilePath('/onemore'), ''); + File::put($cacher->getFilePath('/two'), ''); + + $cacher->invalidateUrl('/one', 'http://example.com'); + + File::delete($cacher->getFilePath('/one')); + File::delete($cacher->getFilePath('/one?foo=bar')); + File::delete($cacher->getFilePath('/onemore')); + File::delete($cacher->getFilePath('/two')); + + $writer->shouldHaveReceived('delete')->times(2); + $writer->shouldHaveReceived('delete')->with($cacher->getFilePath('/one'))->once(); + $writer->shouldHaveReceived('delete')->with($cacher->getFilePath('/one?foo=bar'))->once(); + } + #[Test] #[DataProvider('invalidateEventProvider')] public function invalidating_a_url_dispatches_event($domain, $expectedUrl) From 2344760e0604d7e2bea8113a3a69183be54184d9 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 19 Jun 2025 21:27:01 +0200 Subject: [PATCH 253/490] [5.x] Fix read-only state of roles and groups fields (#11867) --- src/Http/Controllers/CP/Users/UsersController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/Users/UsersController.php b/src/Http/Controllers/CP/Users/UsersController.php index 1534aa46c8b..1d63059b5a8 100644 --- a/src/Http/Controllers/CP/Users/UsersController.php +++ b/src/Http/Controllers/CP/Users/UsersController.php @@ -227,11 +227,11 @@ public function edit(Request $request, $user) $blueprint = $user->blueprint(); if (! User::current()->can('assign roles')) { - $blueprint->ensureField('roles', ['visibility' => 'read_only']); + $blueprint->ensureFieldHasConfig('roles', ['visibility' => 'hidden']); } if (! User::current()->can('assign user groups')) { - $blueprint->ensureField('groups', ['visibility' => 'read_only']); + $blueprint->ensureFieldHasConfig('groups', ['visibility' => 'hidden']); } if (User::current()->isSuper() && User::current()->id() !== $user->id()) { From 6aa60816d1917605277b6a9ddf903f5e2e5ff126 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Thu, 19 Jun 2025 20:40:34 +0100 Subject: [PATCH 254/490] [5.x] Render markdown after antlers when smartypants is enabled (#11814) --- src/Fields/Fieldtype.php | 5 ++++ src/Fields/Value.php | 21 +++++++++++++---- src/Fieldtypes/Markdown.php | 5 ++++ tests/Fields/ValueTest.php | 38 +++++++++++++++++++++++++++++++ tests/Fieldtypes/MarkdownTest.php | 19 ++++++++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Fields/Fieldtype.php b/src/Fields/Fieldtype.php index 113d1edd580..f7c6a400bd6 100644 --- a/src/Fields/Fieldtype.php +++ b/src/Fields/Fieldtype.php @@ -390,4 +390,9 @@ public function extraRenderableFieldData(): array { return []; } + + public function shouldParseAntlersFromRawString(): bool + { + return false; + } } diff --git a/src/Fields/Value.php b/src/Fields/Value.php index b4d2c12ba05..8e17639a736 100644 --- a/src/Fields/Value.php +++ b/src/Fields/Value.php @@ -82,11 +82,14 @@ public function value() $raw = $this->fieldtype->field()?->defaultValue() ?? null; } - $value = $this->shallow + return $this->getAugmentedValue($raw); + } + + private function getAugmentedValue($raw) + { + return $this->shallow ? $this->fieldtype->shallowAugment($raw) : $this->fieldtype->augment($raw); - - return $value; } private function iteratorValue() @@ -150,9 +153,19 @@ public function antlersValue(Parser $parser, $variables) } if ($shouldParseAntlers) { + if ($parseFromRawString = $this->fieldtype?->shouldParseAntlersFromRawString()) { + $value = $this->raw(); + } + $value = (new DocumentTransformer())->correct($value); - return $parser->parse($value, $variables); + $parsed = $parser->parse($value, $variables); + + if (! $parseFromRawString) { + return $parsed; + } + + return $this->getAugmentedValue($parsed); } if (Str::contains($value, '{')) { diff --git a/src/Fieldtypes/Markdown.php b/src/Fieldtypes/Markdown.php index 4acc544a946..035b7cfc7a1 100644 --- a/src/Fieldtypes/Markdown.php +++ b/src/Fieldtypes/Markdown.php @@ -198,4 +198,9 @@ public function preload() 'previewUrl' => cp_route('markdown.preview'), ]; } + + public function shouldParseAntlersFromRawString(): bool + { + return $this->config('smartypants', false); + } } diff --git a/tests/Fields/ValueTest.php b/tests/Fields/ValueTest.php index f0ae32ad9df..407ba9ff5d8 100644 --- a/tests/Fields/ValueTest.php +++ b/tests/Fields/ValueTest.php @@ -10,6 +10,7 @@ use Statamic\Fields\Value; use Statamic\Fields\Values; use Statamic\Query\Builder; +use Statamic\View\Antlers\AntlersString; use Tests\TestCase; class ValueTest extends TestCase @@ -387,6 +388,43 @@ public function it_can_proxy_property_access_to_value() $this->assertEquals('foo', $value->bar); $this->assertEquals('nope', $value->baz ?? 'nope'); } + + #[Test] + public function it_parses_from_raw_string() + { + $fieldtype = new class extends Fieldtype + { + public function augment($data) + { + // if we are being asked to augment an already parsed antlers string + // then we return the correct value + if ($data instanceof AntlersString) { + return 'augmented_value'; + } + + return 'not_augmented_value'; + } + + public function config(?string $key = null, $fallback = null) + { + if ($key == 'antlers') { + return true; + } + + return parent::config($key, $fallback); + } + + public function shouldParseAntlersFromRawString(): bool + { + return true; + } + }; + + $value = new Value('raw_value', null, $fieldtype); + $value = $value->antlersValue(app(\Statamic\Contracts\View\Antlers\Parser::class), []); + + $this->assertEquals('augmented_value', (string) $value); + } } class DummyAugmentable implements \Statamic\Contracts\Data\Augmentable diff --git a/tests/Fieldtypes/MarkdownTest.php b/tests/Fieldtypes/MarkdownTest.php index 8d74f0e5799..ae4848da8b7 100644 --- a/tests/Fieldtypes/MarkdownTest.php +++ b/tests/Fieldtypes/MarkdownTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Facades; use Statamic\Fields\Field; +use Statamic\Fields\Value; use Statamic\Fieldtypes\Markdown; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -170,6 +171,24 @@ public function it_converts_statamic_asset_urls() $this->assertEquals($expected, $this->fieldtype()->augment($markdown)); } + #[Test] + public function it_converts_to_smartypants_after_antlers_is_parsed() + { + $md = $this->fieldtype(['smartypants' => true, 'antlers' => true]); + + $value = <<<'EOT' +{{ "this is a string" | replace(" is ", " isnt ") | reverse }} +EOT; + + $value = new Value($value, 'markdown', $md); + + $expected = <<<'EOT' +

gnirts a tnsi siht

+EOT; + + $this->assertEqualsTrimmed($expected, $value->antlersValue(app(\Statamic\Contracts\View\Antlers\Parser::class), [])); + } + private function fieldtype($config = []) { return (new Markdown)->setField(new Field('test', array_merge(['type' => 'markdown'], $config))); From 6f2a5fbae8a2016b37358458b850249e2862daa2 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 19 Jun 2025 14:59:39 -0500 Subject: [PATCH 255/490] [5.x] Detect recursion when augmenting Entries (#11854) --- src/Data/HasOrigin.php | 22 +++++++++++++++++++ .../RecursiveAugmentationException.php | 7 ++++++ tests/Data/Entries/EntryTest.php | 20 +++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 src/Exceptions/RecursiveAugmentationException.php diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index fe045594ee8..2963b0dd2c4 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -2,10 +2,13 @@ namespace Statamic\Data; +use Statamic\Exceptions\RecursiveAugmentationException; use Statamic\Facades\Blink; trait HasOrigin { + private $resolvingValues = false; + /** * @var string */ @@ -31,14 +34,33 @@ public function keys() ->merge($computedKeys); } + private function guardRecursiveAugmentation() + { + if ($this->resolvingValues) { + $className = get_class($this); + throw new RecursiveAugmentationException("Recursion detected while augmenting [{$className}] with ID [{$this->id}]."); + } + + $this->resolvingValues = true; + } + + private function ungardRecursiveAugmentation() + { + $this->resolvingValues = false; + } + public function values() { + $this->guardRecursiveAugmentation(); + $originFallbackValues = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues() : collect(); $originValues = $this->hasOrigin() ? $this->origin()->values() : collect(); $computedData = method_exists($this, 'computedData') ? $this->computedData() : []; + $this->ungardRecursiveAugmentation(); + return collect() ->merge($originFallbackValues) ->merge($originValues) diff --git a/src/Exceptions/RecursiveAugmentationException.php b/src/Exceptions/RecursiveAugmentationException.php new file mode 100644 index 00000000000..2e0f4aaee69 --- /dev/null +++ b/src/Exceptions/RecursiveAugmentationException.php @@ -0,0 +1,7 @@ +assertEquals('A', $entry->getSupplement('bar')); $this->assertEquals('B', $clone->getSupplement('bar')); } + + #[Test] + public function it_detects_recursive_augmentation() + { + $this->expectException(RecursiveAugmentationException::class); + $this->expectExceptionMessage('Recursion detected while augmenting [Statamic\Entries\Entry] with ID [entry-id]'); + + \Statamic\Facades\Collection::computed('test', 'the_value', function ($entry) { + // Trigger recursion that will bypass without computed values. + return $entry->routeData(); + }); + + $entry = EntryFactory::id('entry-id') + ->collection('test') + ->slug('entry-slug') + ->create(); + + $entry->values(); + } } From c2c5ccd8efdf106f7c3563a037355dcfb595de3a Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 20 Jun 2025 10:19:24 -0400 Subject: [PATCH 256/490] changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36540c46da7..f46541b8025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Release Notes +## 5.58.0 (2025-06-20) + +### What's new +- Detect recursion when augmenting Entries [#11854](https://github.com/statamic/cms/issues/11854) by @JohnathonKoster +- Add `hasField` method to `Fieldset` [#11882](https://github.com/statamic/cms/issues/11882) by @duncanmcclean +- Estonian translations [#11886](https://github.com/statamic/cms/issues/11886) by @karlromets + +### What's fixed +- Render markdown after antlers when smartypants is enabled [#11814](https://github.com/statamic/cms/issues/11814) by @ryanmitchell +- Fix read-only state of roles and groups fields [#11867](https://github.com/statamic/cms/issues/11867) by @aerni +- Fix files not being removed after cache has been cleared [#11873](https://github.com/statamic/cms/issues/11873) by @indykoning +- Ensure nav blueprint graphql types are registered [#11881](https://github.com/statamic/cms/issues/11881) by @ryanmitchell +- Fix issues with Blade nav tag compiler [#11872](https://github.com/statamic/cms/issues/11872) by @JohnathonKoster +- Ensure propagating entries respects saveQuietly [#11875](https://github.com/statamic/cms/issues/11875) by @ryanmitchell +- Fix authorization error when creating globals [#11883](https://github.com/statamic/cms/issues/11883) by @duncanmcclean +- Fixes typo [#11876](https://github.com/statamic/cms/issues/11876) by @adampatterson +- Updated `AddonServiceProvider::shouldBootRootItems()` to support trailing slashes [#11861](https://github.com/statamic/cms/issues/11861) by @simonworkhouse +- Prevent null in strtolower() [#11869](https://github.com/statamic/cms/issues/11869) by @martinoak +- Ensure Glide treats asset urls starting with the app url as internal assets [#11839](https://github.com/statamic/cms/issues/11839) by @marcorieser +- Remove single quote in Asset upload [#11858](https://github.com/statamic/cms/issues/11858) by @adampatterson + + + ## 5.57.0 (2025-06-04) ### What's new From 224d1ef72d03696ab2fc684b0d4a806cb5d2c705 Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 24 Jun 2025 10:14:13 -0500 Subject: [PATCH 257/490] [5.x] Revert detect recursion when augmenting Entries (#11854) (#11894) --- src/Data/HasOrigin.php | 22 ------------------- .../RecursiveAugmentationException.php | 7 ------ tests/Data/Entries/EntryTest.php | 20 ----------------- 3 files changed, 49 deletions(-) delete mode 100644 src/Exceptions/RecursiveAugmentationException.php diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 2963b0dd2c4..fe045594ee8 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -2,13 +2,10 @@ namespace Statamic\Data; -use Statamic\Exceptions\RecursiveAugmentationException; use Statamic\Facades\Blink; trait HasOrigin { - private $resolvingValues = false; - /** * @var string */ @@ -34,33 +31,14 @@ public function keys() ->merge($computedKeys); } - private function guardRecursiveAugmentation() - { - if ($this->resolvingValues) { - $className = get_class($this); - throw new RecursiveAugmentationException("Recursion detected while augmenting [{$className}] with ID [{$this->id}]."); - } - - $this->resolvingValues = true; - } - - private function ungardRecursiveAugmentation() - { - $this->resolvingValues = false; - } - public function values() { - $this->guardRecursiveAugmentation(); - $originFallbackValues = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues() : collect(); $originValues = $this->hasOrigin() ? $this->origin()->values() : collect(); $computedData = method_exists($this, 'computedData') ? $this->computedData() : []; - $this->ungardRecursiveAugmentation(); - return collect() ->merge($originFallbackValues) ->merge($originValues) diff --git a/src/Exceptions/RecursiveAugmentationException.php b/src/Exceptions/RecursiveAugmentationException.php deleted file mode 100644 index 2e0f4aaee69..00000000000 --- a/src/Exceptions/RecursiveAugmentationException.php +++ /dev/null @@ -1,7 +0,0 @@ -assertEquals('A', $entry->getSupplement('bar')); $this->assertEquals('B', $clone->getSupplement('bar')); } - - #[Test] - public function it_detects_recursive_augmentation() - { - $this->expectException(RecursiveAugmentationException::class); - $this->expectExceptionMessage('Recursion detected while augmenting [Statamic\Entries\Entry] with ID [entry-id]'); - - \Statamic\Facades\Collection::computed('test', 'the_value', function ($entry) { - // Trigger recursion that will bypass without computed values. - return $entry->routeData(); - }); - - $entry = EntryFactory::id('entry-id') - ->collection('test') - ->slug('entry-slug') - ->create(); - - $entry->values(); - } } From c3fefc29de5cb6aa1edf8d5a44251d5e4f63081d Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Wed, 25 Jun 2025 17:08:38 +0200 Subject: [PATCH 258/490] [5.x] Fix Overflow buttons preview (#11891) Co-authored-by: Jason Varga --- resources/js/components/entries/PublishForm.vue | 6 +++--- resources/js/components/terms/PublishForm.vue | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/js/components/entries/PublishForm.vue b/resources/js/components/entries/PublishForm.vue index db0d4477da6..eb90849f6b2 100644 --- a/resources/js/components/entries/PublishForm.vue +++ b/resources/js/components/entries/PublishForm.vue @@ -118,16 +118,16 @@
-
+
diff --git a/resources/js/components/terms/PublishForm.vue b/resources/js/components/terms/PublishForm.vue index 9e12d6ebdb3..002a4ce9185 100644 --- a/resources/js/components/terms/PublishForm.vue +++ b/resources/js/components/terms/PublishForm.vue @@ -112,16 +112,16 @@
-
+
From c447d6e42291f35f5c0f6881a886a810d3e9e379 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 25 Jun 2025 11:31:36 -0400 Subject: [PATCH 259/490] [5.x] Add entry serialization test (#11900) --- tests/Data/Entries/EntryTest.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index bffe08eef18..583fb05001b 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -2678,4 +2678,31 @@ public function it_clones_internal_collections() $this->assertEquals('A', $entry->getSupplement('bar')); $this->assertEquals('B', $clone->getSupplement('bar')); } + + #[Test] + public function entries_can_be_serialized_after_resolving_values() + { + $entry = EntryFactory::id('entry-id') + ->collection('test') + ->slug('entry-slug') + ->create(); + + $customEntry = CustomEntry::fromEntry($entry); + + $serialized = serialize($customEntry); + $unserialized = unserialize($serialized); + + $this->assertSame('entry-slug', $unserialized->slug); + } +} + +class CustomEntry extends Entry +{ + public static function fromEntry(Entry $entry) + { + return (new static) + ->slug($entry->slug) + ->collection($entry->collection) + ->data($entry->data); + } } From 3b73a795a10900a7a2a1a6748b0383faa5e1b230 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 25 Jun 2025 11:32:55 -0400 Subject: [PATCH 260/490] changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f46541b8025..0d6d0663c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Release Notes +## 5.58.1 (2025-06-25) + +### What's fixed +- Fix Overflow buttons preview [#11891](https://github.com/statamic/cms/issues/11891) by @marcorieser +- Revert detect recursion when augmenting Entries (#11854) [#11894](https://github.com/statamic/cms/issues/11894) by @JohnathonKoster +- Add entry serialization test [#11900](https://github.com/statamic/cms/issues/11900) by @jasonvarga + + + ## 5.58.0 (2025-06-20) ### What's new From ddc1a27a6298414bfb3251f67a6255ccfb28f390 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 30 Jun 2025 10:07:45 +0100 Subject: [PATCH 261/490] [5.x] Fix casing on dropdown item (#11907) Fix casing on dropdown item We already use "Edit Nav Item" elsewhere, so fixing the casing here to prevent us needing duplicate translations. --- resources/js/components/navigation/View.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/navigation/View.vue b/resources/js/components/navigation/View.vue index ac58beecba0..89c15278680 100644 --- a/resources/js/components/navigation/View.vue +++ b/resources/js/components/navigation/View.vue @@ -109,7 +109,7 @@ :text="__('Edit Entry')" :redirect="branch.edit_url" /> Date: Mon, 30 Jun 2025 11:21:28 +0200 Subject: [PATCH 262/490] [5.x] German translations (#11903) * German translations * remove duplicate translation not needed after #11907 --------- Co-authored-by: Duncan McClean --- resources/lang/de.json | 4 ++++ resources/lang/de/fieldtypes.php | 3 ++- resources/lang/de/validation.php | 1 + resources/lang/de_CH.json | 5 +++++ resources/lang/de_CH/fieldtypes.php | 3 ++- resources/lang/de_CH/validation.php | 1 + 6 files changed, 15 insertions(+), 2 deletions(-) diff --git a/resources/lang/de.json b/resources/lang/de.json index 8266c8aded8..a5f50c87c76 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -768,6 +768,7 @@ "Released on :date": "Veröffentlicht am :date", "Remember me": "An mich erinnern", "Remove": "Entfernen", + "Remove All": "Alle entfernen", "Remove all empty nodes": "Alle leeren Nodes entfernen", "Remove Asset": "Datei entfernen", "Remove child page|Remove :count child pages": "Untergeordnete Seite entfernen|:count untergeordnete Seiten entfernen", @@ -911,6 +912,7 @@ "Stacked": "Gestapelt", "Start Impersonating": "Nachahmung beginnen", "Start Page": "Startseite", + "Start typing to search.": "Zum Suchen mit Tippen beginnen.", "Statamic": "Statamic", "Statamic Pro is required.": "Statamic Pro ist erforderlich.", "Static Page Cache": "Statischer Seitencache", @@ -950,6 +952,7 @@ "Taxonomy saved": "Taxonomie gespeichert", "Template": "Template", "Templates": "Templates", + "Term": "Begriff", "Term created": "Begriff erstellt", "Term deleted": "Begriff gelöscht", "Term references updated": "Verweise auf Begriffe aktualisiert", @@ -1010,6 +1013,7 @@ "Uncheck All": "Alles abwählen", "Underline": "Unterstrichen", "Unlink": "Verknüpfung aufheben", + "Unlink All": "Alle Verknüpfung aufheben", "Unlisted Addons": "Nicht aufgelistete Addons", "Unordered List": "Ungeordnete Liste", "Unpin from Favorites": "Aus Favoriten entfernen", diff --git a/resources/lang/de/fieldtypes.php b/resources/lang/de/fieldtypes.php index 8776c04c4a3..cb921262851 100644 --- a/resources/lang/de/fieldtypes.php +++ b/resources/lang/de/fieldtypes.php @@ -44,6 +44,7 @@ 'bard.config.section.editor.instructions' => 'Das Aussehen und das grundsätzliche Verhalten des Editors konfigurieren.', 'bard.config.section.links.instructions' => 'Legt fest, wie Links in dieser Instanz von Bard behandelt werden.', 'bard.config.section.sets.instructions' => 'Konfiguriere Blöcke von Feldern, die an beliebiger Stelle in deinen Bard-Inhalt eingefügt werden können.', + 'bard.config.select_across_sites' => 'Erlaubt die Auswahl von Einträgen anderer Websites. Dadurch werden auch die Lokalisierungsoptionen im Frontend deaktiviert. Mehr dazu in der [Dokumentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', 'bard.config.smart_typography' => 'Gängige Textmuster mit den richtigen typografischen Zeichen ersetzen.', 'bard.config.target_blank' => 'Sämtliche Links mit `target="_blank"` ausstatten.', 'bard.config.toolbar_mode' => 'Im **fixierten** Modus bleibt die Symbolleiste immer sichtbar, während sie im **schwebenden** Modus nur bei der Textauswahl erscheint.', @@ -85,7 +86,7 @@ 'entries.config.create' => 'Das Erstellen neuer Einträge zulassen.', 'entries.config.query_scopes' => 'Auswählen, welche Abfragebereiche beim Abrufen der auswählbaren Einträge angewendet werden sollen.', 'entries.config.search_index' => 'Ein geeigneter Suchindex wird nach Möglichkeit automatisch verwendet. Du kannst aber auch einen eigenen definieren.', - 'entries.config.select_across_sites' => 'Erlaubt die Auswahl von Einträgen von anderen Websites. Dadurch werden auch die Lokalisierungsoptionen im Frontend deaktiviert. Mehr dazu in der [Dokumentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', + 'entries.config.select_across_sites' => 'Erlaubt die Auswahl von Einträgen anderer Websites. Dadurch werden auch die Lokalisierungsoptionen im Frontend deaktiviert. Mehr dazu in der [Dokumentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', 'entries.title' => 'Einträge', 'float.title' => 'Gleitkommazahl', 'form.config.max_items' => 'Maximale Anzahl auswählbarer Formulare auswählen.', diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php index fd572675955..affbe114885 100644 --- a/resources/lang/de/validation.php +++ b/resources/lang/de/validation.php @@ -118,6 +118,7 @@ 'uuid' => 'Muss eine gültige UUID sein.', 'arr_fieldtype' => 'Dies ist ungültig.', 'handle' => 'Darf nur Kleinbuchstaben und Zahlen mit Unterstrichen als Trennzeichen enthalten.', + 'handle_starts_with_number' => 'Darf nicht mit einer Zahl beginnen.', 'slug' => 'Darf nur Buchstaben und Zahlen mit Bindestrichen oder Unterstrichen als Trennzeichen enthalten.', 'code_fieldtype_rulers' => 'Dies ist ungültig.', 'composer_package' => 'Muss ein gültiger Composer-Paketname sein (z.B. hasselhoff/kung-fury).', diff --git a/resources/lang/de_CH.json b/resources/lang/de_CH.json index 6da9859473a..8326b2bc1f4 100644 --- a/resources/lang/de_CH.json +++ b/resources/lang/de_CH.json @@ -377,6 +377,7 @@ "Edit Global Set": "Globales Set bearbeiten", "Edit Image": "Bild bearbeiten", "Edit Nav Item": "Navigationselement bearbeiten", + "Edit nav Item": "Navigationselement bearbeiten", "Edit Navigation": "Navigation bearbeiten", "Edit Section": "Abschnitt bearbeiten", "Edit Set": "Set bearbeiten", @@ -768,6 +769,7 @@ "Released on :date": "Veröffentlicht am :date", "Remember me": "An mich erinnern", "Remove": "Entfernen", + "Remove All": "Alle entfernen", "Remove all empty nodes": "Alle leeren Nodes entfernen", "Remove Asset": "Datei entfernen", "Remove child page|Remove :count child pages": "Untergeordnete Seite entfernen|:count untergeordnete Seiten entfernen", @@ -911,6 +913,7 @@ "Stacked": "Gestapelt", "Start Impersonating": "Nachahmung beginnen", "Start Page": "Startseite", + "Start typing to search.": "Zum Suchen mit Tippen beginnen.", "Statamic": "Statamic", "Statamic Pro is required.": "Statamic Pro ist erforderlich.", "Static Page Cache": "Statischer Seitencache", @@ -950,6 +953,7 @@ "Taxonomy saved": "Taxonomie gespeichert", "Template": "Template", "Templates": "Templates", + "Term": "Begriff", "Term created": "Begriff erstellt", "Term deleted": "Begriff gelöscht", "Term references updated": "Verweise auf Begriffe aktualisiert", @@ -1010,6 +1014,7 @@ "Uncheck All": "Alles abwählen", "Underline": "Unterstrichen", "Unlink": "Verknüpfung aufheben", + "Unlink All": "Alle Verknüpfung aufheben", "Unlisted Addons": "Nicht aufgelistete Addons", "Unordered List": "Ungeordnete Liste", "Unpin from Favorites": "Aus Favoriten entfernen", diff --git a/resources/lang/de_CH/fieldtypes.php b/resources/lang/de_CH/fieldtypes.php index fe2d370dfc6..e55c71a1ba0 100644 --- a/resources/lang/de_CH/fieldtypes.php +++ b/resources/lang/de_CH/fieldtypes.php @@ -44,6 +44,7 @@ 'bard.config.section.editor.instructions' => 'Das Aussehen und das grundsätzliche Verhalten des Editors konfigurieren.', 'bard.config.section.links.instructions' => 'Legt fest, wie Links in dieser Instanz von Bard behandelt werden.', 'bard.config.section.sets.instructions' => 'Konfiguriere Blöcke von Feldern, die an beliebiger Stelle in deinen Bard-Inhalt eingefügt werden können.', + 'bard.config.select_across_sites' => 'Erlaubt die Auswahl von Einträgen anderer Websites. Dadurch werden auch die Lokalisierungsoptionen im Frontend deaktiviert. Mehr dazu in der [Dokumentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', 'bard.config.smart_typography' => 'Gängige Textmuster mit den richtigen typografischen Zeichen ersetzen.', 'bard.config.target_blank' => 'Sämtliche Links mit `target="_blank"` ausstatten.', 'bard.config.toolbar_mode' => 'Im **fixierten** Modus bleibt die Symbolleiste immer sichtbar, während sie im **schwebenden** Modus nur bei der Textauswahl erscheint.', @@ -85,7 +86,7 @@ 'entries.config.create' => 'Das Erstellen neuer Einträge zulassen.', 'entries.config.query_scopes' => 'Auswählen, welche Abfragebereiche beim Abrufen der auswählbaren Einträge angewendet werden sollen.', 'entries.config.search_index' => 'Ein geeigneter Suchindex wird nach Möglichkeit automatisch verwendet. Du kannst aber auch einen eigenen definieren.', - 'entries.config.select_across_sites' => 'Erlaubt die Auswahl von Einträgen von anderen Websites. Dadurch werden auch die Lokalisierungsoptionen im Frontend deaktiviert. Mehr dazu in der [Dokumentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', + 'entries.config.select_across_sites' => 'Erlaubt die Auswahl von Einträgen anderer Websites. Dadurch werden auch die Lokalisierungsoptionen im Frontend deaktiviert. Mehr dazu in der [Dokumentation](https://statamic.dev/fieldtypes/entries#select-across-sites).', 'entries.title' => 'Einträge', 'float.title' => 'Gleitkommazahl', 'form.config.max_items' => 'Maximale Anzahl auswählbarer Formulare auswählen.', diff --git a/resources/lang/de_CH/validation.php b/resources/lang/de_CH/validation.php index cc409dd73e9..4915db65132 100644 --- a/resources/lang/de_CH/validation.php +++ b/resources/lang/de_CH/validation.php @@ -118,6 +118,7 @@ 'uuid' => 'Muss eine gültige UUID sein.', 'arr_fieldtype' => 'Dies ist ungültig.', 'handle' => 'Darf nur Kleinbuchstaben und Zahlen mit Unterstrichen als Trennzeichen enthalten.', + 'handle_starts_with_number' => 'Darf nicht mit einer Zahl beginnen.', 'slug' => 'Darf nur Buchstaben und Zahlen mit Bindestrichen oder Unterstrichen als Trennzeichen enthalten.', 'code_fieldtype_rulers' => 'Dies ist ungültig.', 'composer_package' => 'Muss ein gültiger Composer-Paketname sein (z.B. hasselhoff/kung-fury).', From d7624c784d473dec9a6508dc04e41889ca69df93 Mon Sep 17 00:00:00 2001 From: Manuel Pirker-Ihl Date: Tue, 1 Jul 2025 13:36:02 +0200 Subject: [PATCH 263/490] [5.x] Class "DB" not found issue (#11911) * fix: Class "DB" not found issue * fix: lint issue --- src/Auth/Eloquent/UserGroup.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Auth/Eloquent/UserGroup.php b/src/Auth/Eloquent/UserGroup.php index e2ef88ea887..e75268b5c83 100644 --- a/src/Auth/Eloquent/UserGroup.php +++ b/src/Auth/Eloquent/UserGroup.php @@ -2,6 +2,7 @@ namespace Statamic\Auth\Eloquent; +use Illuminate\Support\Facades\DB; use Statamic\Auth\File\UserGroup as FileUserGroup; use Statamic\Facades\User; @@ -50,7 +51,7 @@ public function queryUsers() protected function getUserIds() { - return \DB::connection(config('statamic.users.database')) + return DB::connection(config('statamic.users.database')) ->table(config('statamic.users.tables.group_user', 'group_user')) ->where('group_id', $this->id()) ->pluck('user_id'); From 6a3aa2ce3ed8d03dce1dc735327668b9fb82ba93 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 4 Jul 2025 17:03:43 +0100 Subject: [PATCH 264/490] [5.x] Prevent group fieldtype from filtering out `false` values (#11928) --- src/Fieldtypes/Group.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Fieldtypes/Group.php b/src/Fieldtypes/Group.php index 9b70ebea085..2abd8c9d9d9 100644 --- a/src/Fieldtypes/Group.php +++ b/src/Fieldtypes/Group.php @@ -7,6 +7,7 @@ use Statamic\Fields\Fieldtype; use Statamic\Fields\Values; use Statamic\GraphQL\Types\GroupType; +use Statamic\Support\Arr; use Statamic\Support\Str; class Group extends Fieldtype @@ -50,7 +51,9 @@ protected function configFieldItems(): array public function process($data) { - return $this->fields()->addValues($data ?? [])->process()->values()->filter()->all(); + $values = $this->fields()->addValues($data ?? [])->process()->values()->all(); + + return Arr::removeNullValues($values); } public function preProcess($data) From 7e1355ea7e26d525e879b685e64bea9e8d9842c7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 4 Jul 2025 17:04:03 +0100 Subject: [PATCH 265/490] [5.x] Relax strict type check in `Tree::move()` (#11927) --- src/Structures/Tree.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Structures/Tree.php b/src/Structures/Tree.php index 1caff9d5d4d..1cbd7e1270e 100644 --- a/src/Structures/Tree.php +++ b/src/Structures/Tree.php @@ -310,7 +310,7 @@ public function move($entry, $target) { $parent = optional($this->find($entry)->parent()); - if ($parent->id() === $target || $parent->isRoot() && is_null($target)) { + if ($parent->id() == $target || $parent->isRoot() && is_null($target)) { return $this; } From df2b4fa0b8c31c442e0e7ceddbff60784091b998 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 4 Jul 2025 12:04:59 -0400 Subject: [PATCH 266/490] [5.x] Prompt to update search indexes when installing starter kits (#11924) --- src/Console/Commands/StarterKitInstall.php | 36 ++++++++++++- src/Search/Null/NullSearchables.php | 7 +++ tests/StarterKits/InstallTest.php | 62 +++++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/Console/Commands/StarterKitInstall.php b/src/Console/Commands/StarterKitInstall.php index 3dc63df0887..6d9bbd772b2 100644 --- a/src/Console/Commands/StarterKitInstall.php +++ b/src/Console/Commands/StarterKitInstall.php @@ -32,7 +32,8 @@ class StarterKitInstall extends Command { --without-user : Install without creating user } { --force : Force install and allow dependency errors } { --cli-install : Installing from CLI Tool } - { --clear-site : Clear site before installing }'; + { --clear-site : Clear site before installing } + { --update-search : Update search index(es) after installing }'; /** * The console command description. @@ -80,6 +81,10 @@ public function handle() return 1; } + if ($this->shouldUpdateSearchIndex()) { + $this->updateSearchIndex(); + } + // Temporary prompt to inform user of updated CLI tool. The newest version has better messaging // around paid starter kit licenses, so we want to push users to upgrade to minimize support // requests around expired licenses. The newer version of the CLI tool will also notify @@ -139,6 +144,35 @@ protected function clearSite(): void Prompt::interactive($this->input->isInteractive()); } + /** + * Check if should update search index. + */ + protected function shouldUpdateSearchIndex(): bool + { + if ($this->option('update-search')) { + return true; + } elseif ($this->input->isInteractive()) { + return confirm('Would you like to update your search index(es) as well?', false); + } + + return false; + } + + /** + * Update search index, and re-set prompt interactivity for future prompts. + * + * See: https://github.com/statamic/cli/issues/62 + */ + protected function updateSearchIndex(): void + { + $this->call('statamic:search:update', [ + '--all' => true, + '--no-interaction' => true, + ]); + + Prompt::interactive($this->input->isInteractive()); + } + /** * Detect older Statamic CLI installation. */ diff --git a/src/Search/Null/NullSearchables.php b/src/Search/Null/NullSearchables.php index a2e1b90d04e..c63ab192419 100644 --- a/src/Search/Null/NullSearchables.php +++ b/src/Search/Null/NullSearchables.php @@ -2,10 +2,17 @@ namespace Statamic\Search\Null; +use Illuminate\Support\LazyCollection; + class NullSearchables { public function contains() { return false; } + + public function lazy(): LazyCollection + { + return LazyCollection::make(); + } } diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 6fc18b8aa11..9f4e8a83d98 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -14,6 +14,7 @@ use Statamic\Facades\Blink; use Statamic\Facades\Config; use Statamic\Facades\Path; +use Statamic\Facades\Search; use Statamic\Facades\YAML; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -495,7 +496,8 @@ public function it_clears_site_when_interactively_confirmed() $this ->installCoolRunningsInteractively(['--without-user' => true]) - ->expectsConfirmation('Clear site first?', 'yes'); + ->expectsConfirmation('Clear site first?', 'yes') + ->expectsConfirmation('Would you like to update your search index(es) as well?', 'no'); $this->assertFileExists(base_path('content/collections/pages/home.md')); $this->assertFileDoesNotExist(base_path('content/collections/pages/contact.md')); @@ -1637,6 +1639,63 @@ public function it_installs_nested_modules_confirmed_interactively_via_prompt() $this->assertComposerJsonHasPackageVersion('require', 'bobsled/speed-calculator', '^1.0.0'); } + #[Test] + public function it_doesnt_update_search_index_by_default_when_installed_non_interactively() + { + Search::shouldReceive('indexes')->never(); + + $this + ->installCoolRunnings() + ->assertSuccessful(); + + $this->assertFileExists(base_path('copied.md')); + } + + #[Test] + public function it_updates_search_index_when_update_search_flag_is_passed() + { + Search::shouldReceive('indexes') + ->once() + ->andReturn([]); + + $this + ->installCoolRunnings(['--update-search' => true]) + ->assertSuccessful(); + + $this->assertFileExists(base_path('copied.md')); + } + + #[Test] + public function it_doesnt_update_search_index_by_default_when_installed_interactively() + { + Search::shouldReceive('indexes')->never(); + + $this + ->installCoolRunningsInteractively() + ->expectsConfirmation('Clear site first?', 'no') + ->expectsConfirmation('Would you like to update your search index(es) as well?', 'no') + ->doesntExpectOutput('statamic:search:update') + ->assertSuccessful(); + + $this->assertFileExists(base_path('copied.md')); + } + + #[Test] + public function it_updates_search_index_when_installed_interactively_confirmed() + { + Search::shouldReceive('indexes') + ->once() + ->andReturn([]); + + $this + ->installCoolRunningsInteractively() + ->expectsConfirmation('Clear site first?', 'no') + ->expectsConfirmation('Would you like to update your search index(es) as well?', 'yes') + ->assertSuccessful(); + + $this->assertFileExists(base_path('copied.md')); + } + private function kitRepoPath($path = null) { return Path::tidy(collect([base_path('repo/cool-runnings'), $path])->filter()->implode('/')); @@ -1692,6 +1751,7 @@ private function installCoolRunningsModules($options = [], $customHttpFake = nul return $this->installCoolRunningsInteractively(array_merge($options, [ '--clear-site' => true, // skip clear site prompt '--without-user' => true, // skip create user prompt + '--update-search' => true, // skip update search index prompt ]), $customHttpFake); } From dc675113611a618b851cf446e069e1a6bed8a722 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Fri, 4 Jul 2025 18:06:44 +0200 Subject: [PATCH 267/490] [5.x] Allow fetching children of other entries in `{{ children }}` tag (#11922) --- src/Tags/Children.php | 6 ++++-- tests/Tags/ChildrenTest.php | 26 ++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Tags/Children.php b/src/Tags/Children.php index 40d1fd6b4ca..bbcdc060933 100644 --- a/src/Tags/Children.php +++ b/src/Tags/Children.php @@ -11,13 +11,15 @@ class Children extends Structure /** * The {{ children }} tag. * - * Get any children of the current url + * Get any children of the current or a specified url. * * @return string */ public function index() { - $this->params->put('from', Str::start(Str::after(URL::makeAbsolute(URL::getCurrent()), Site::current()->absoluteUrl()), '/')); + $url = $this->params->get('of', URL::getCurrent()); + + $this->params->put('from', Str::start(Str::after(URL::makeAbsolute($url), Site::current()->absoluteUrl()), '/')); $this->params->put('max_depth', 1); $collection = $this->params->get('collection', $this->context->value('collection')?->handle()); diff --git a/tests/Tags/ChildrenTest.php b/tests/Tags/ChildrenTest.php index e8297eb90e8..7dc283db9d8 100644 --- a/tests/Tags/ChildrenTest.php +++ b/tests/Tags/ChildrenTest.php @@ -44,6 +44,10 @@ private function setUpEntries() 'title' => 'the bar entry', ])->create(); + EntryFactory::collection('pages')->id('baz')->data([ + 'title' => 'the baz entry', + ])->create(); + EntryFactory::collection('pages')->id('fr-foo')->origin('foo')->locale('fr')->data([ 'title' => 'the french foo entry', ])->create(); @@ -52,15 +56,23 @@ private function setUpEntries() 'title' => 'the french bar entry', ])->create(); + EntryFactory::collection('pages')->id('fr-baz')->origin('foo')->locale('fr')->data([ + 'title' => 'the french baz entry', + ])->create(); + $this->collection->structure()->in('en')->tree([ ['entry' => 'foo', 'url' => '/foo', 'children' => [ - ['entry' => 'bar', 'url' => '/foo/bar'], + ['entry' => 'bar', 'url' => '/foo/bar', 'children' => [ + ['entry' => 'baz', 'url' => '/foo/bar/baz'], + ]], ]], ])->save(); $this->collection->structure()->in('fr')->tree([ ['entry' => 'fr-foo', 'url' => '/fr-foo', 'children' => [ - ['entry' => 'fr-bar', 'url' => '/fr-foo/fr-bar'], + ['entry' => 'fr-bar', 'url' => '/fr-foo/fr-bar', 'children' => [ + ['entry' => 'fr-baz', 'url' => '/fr-foo/fr-bar/fr-baz'], + ]], ]], ])->save(); } @@ -74,6 +86,16 @@ public function it_gets_children_data() $this->assertEquals('the bar entry', $this->tag('{{ children }}{{ title }}{{ /children }}', ['collection' => $this->collection])); } + #[Test] + public function it_gets_children_data_of_another_entry() + { + $this->setUpEntries(); + + $this->get('/foo'); + + $this->assertEquals('the baz entry', $this->tag('{{ children of="/foo/bar" }}{{ title }}{{ /children }}', ['collection' => $this->collection])); + } + #[Test] public function it_gets_children_data_when_in_another_site() { From e50917d5ea5e9f70efff235cb8d8a3ec61d6d8cc Mon Sep 17 00:00:00 2001 From: Marty Friedel Date: Mon, 7 Jul 2025 22:54:29 +0930 Subject: [PATCH 268/490] [5.x] Fix group fieldtype child field validation rules when using {this} within a replicator/grid (#11931) --- src/Fieldtypes/Group.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fieldtypes/Group.php b/src/Fieldtypes/Group.php index 2abd8c9d9d9..bea798351f1 100644 --- a/src/Fieldtypes/Group.php +++ b/src/Fieldtypes/Group.php @@ -78,7 +78,7 @@ public function extraRules(): array ->addValues((array) $this->field->value()) ->validator() ->withContext([ - 'prefix' => $this->field->handle().'.', + 'prefix' => $this->field->validationContext('prefix'), ]) ->rules(); From 8e26a6bd463868d5b03e9cad50051becd02d7049 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 7 Jul 2025 09:25:26 -0400 Subject: [PATCH 269/490] [5.x] Fix visibility of custom nav items for users w/ limited permissions (#11930) Co-authored-by: jesseleite <5187394+jesseleite@users.noreply.github.com> --- src/CP/Navigation/NavBuilder.php | 14 ++++-- tests/CP/Navigation/NavPreferencesTest.php | 55 +++++++++++++++++++++- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/CP/Navigation/NavBuilder.php b/src/CP/Navigation/NavBuilder.php index 4219e56c2e5..17d3c3ad492 100644 --- a/src/CP/Navigation/NavBuilder.php +++ b/src/CP/Navigation/NavBuilder.php @@ -57,13 +57,13 @@ public function build($preferences = true) ->resolveChildrenClosures() ->validateNesting() ->validateViews() - ->authorizeItems() - ->authorizeChildren() ->syncOriginal() ->trackCoreSections() ->trackOriginalSectionItems() ->trackUrls() ->applyPreferenceOverrides($preferences) + ->authorizeItems() + ->authorizeChildren() ->buildSections() ->blinkUrls() ->get(); @@ -156,7 +156,10 @@ protected function authorizeChildren() { collect($this->items) ->reject(fn ($item) => is_callable($item->children())) - ->each(fn ($item) => $item->children($this->filterAuthorizedNavItems($item->children()))); + ->each(fn ($item) => $item->children( + items: $this->filterAuthorizedNavItems($item->children()), + generateNewIds: false, + )); return $this; } @@ -707,7 +710,10 @@ protected function userModifyItemChildren($item, $childrenOverrides, $section, $ $newChildren->each(fn ($item, $index) => $item->order($index + 1)); - $item->children($newChildren, false); + $item->children( + items: $newChildren, + generateNewIds: false, + ); return $newChildren; } diff --git a/tests/CP/Navigation/NavPreferencesTest.php b/tests/CP/Navigation/NavPreferencesTest.php index bdeaffffe75..7d7fe7776c0 100644 --- a/tests/CP/Navigation/NavPreferencesTest.php +++ b/tests/CP/Navigation/NavPreferencesTest.php @@ -5,12 +5,14 @@ use Illuminate\Support\Facades\Request; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades; +use Tests\FakesRoles; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; class NavPreferencesTest extends TestCase { use Concerns\HashedIdAssertions; + use FakesRoles; use PreventSavingStacheItemsToDisk; protected $shouldPreventNavBeingBuilt = false; @@ -1694,9 +1696,58 @@ public function it_checks_active_status_on_moved_items() $this->assertTrue($tags->isActive()); } - private function buildNavWithPreferences($preferences, $withHidden = false) + #[Test] + public function it_can_hide_items_the_user_does_not_have_permission_to_see() { - $this->actingAs(tap(Facades\User::make()->makeSuper())->save()); + $preferences = [ + 'favourites' => [ + 'display' => 'Favourites', + 'items' => [ + 'content::collections::articles' => '@alias', + 'content::collections::pages' => '@alias', + ], + ], + 'author_section' => [ + 'display' => 'Authors', + 'items' => [ + 'content::collections::articles' => '@move', + ], + ], + 'webmaster_section' => [ + 'display' => 'Webmasters', + 'items' => [ + 'content::collections::pages' => '@move', + ], + ], + ]; + + // A super user can see these items... + $nav = $this->buildNavWithPreferences($preferences); + $this->assertCount(2, $nav->get('Favourites')); + $this->assertTrue($nav->get('Favourites')->keyBy->display()->has('Articles')); + $this->assertTrue($nav->get('Favourites')->keyBy->display()->has('Pages')); + $this->assertCount(1, $nav->get('Authors')); + $this->assertTrue($nav->get('Authors')->keyBy->display()->has('Articles')); + $this->assertCount(1, $nav->get('Webmasters')); + $this->assertTrue($nav->get('Webmasters')->keyBy->display()->has('Pages')); + + // But an author with permissions to only view articles... + $this->setTestRoles(['author' => ['view articles entries']]); + $user = Facades\User::make()->assignRole('author'); + + // Should not see pages related section and/or items... + $nav = $this->buildNavWithPreferences($preferences, user: $user); + $this->assertCount(1, $nav->get('Favourites')); + $this->assertTrue($nav->get('Favourites')->keyBy->display()->has('Articles')); + $this->assertFalse($nav->get('Favourites')->keyBy->display()->has('Pages')); + $this->assertCount(1, $nav->get('Authors')); + $this->assertTrue($nav->get('Authors')->keyBy->display()->has('Articles')); + $this->assertFalse($nav->has('Webmasters')); + } + + private function buildNavWithPreferences($preferences, $withHidden = false, $user = null) + { + $this->actingAs(tap($user ?? Facades\User::make()->makeSuper())->save()); return Facades\CP\Nav::build($preferences, $withHidden)->pluck('items', 'display'); } From 8e8ee8da489a4f7466e41c3e432947e08ade6dff Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Mon, 7 Jul 2025 15:44:44 +0200 Subject: [PATCH 270/490] [5.x] `resolve` modifier (#11890) --- src/Modifiers/CoreModifiers.php | 18 ++++++++++ tests/Modifiers/ResolveTest.php | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/Modifiers/ResolveTest.php diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 80a85714f61..1083c09d07e 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -2122,6 +2122,24 @@ public function replace($value, $params) return Stringy::replace($value, Arr::get($params, 0), Arr::get($params, 1)); } + /** + * Resolves a specific index or all items from an array, a Collection, or a Query Builder. + */ + public function resolve($value, $params) + { + $key = Arr::get($params, 0); + + if (Compare::isQueryBuilder($value)) { + $value = $value->get(); + } + + if ($value instanceof Collection) { + $value = $value->all(); + } + + return Arr::get($value, $key); + } + /** * Reverses the order of a string or list. * diff --git a/tests/Modifiers/ResolveTest.php b/tests/Modifiers/ResolveTest.php new file mode 100644 index 00000000000..545368af946 --- /dev/null +++ b/tests/Modifiers/ResolveTest.php @@ -0,0 +1,64 @@ +data(['title' => 'Famous Gandalf quotes']) + ->create(); + + $modified = $this->modify(Entry::query()->where('collection', $collection)); + $this->assertIsArray($modified); + $this->assertEquals($entry, $modified[0]); + + $modified = $this->modify(Entry::query()->where('collection', $collection), [0]); + $this->assertEquals($entry, $modified); + } + + #[Test] + public function it_resolves_a_collection(): void + { + $modified = $this->modify(collect(['You shall not pass!', 'Fool of a Took'])); + $this->assertIsArray($modified); + $this->assertEquals(['You shall not pass!', 'Fool of a Took'], $modified); + + $modified = $this->modify(collect(['You shall not pass!', 'Fool of a Took']), [1]); + $this->assertEquals('Fool of a Took', $modified); + + $modified = $this->modify(collect(['one' => 'You shall not pass!', 'two' => 'Fool of a Took']), ['two']); + $this->assertEquals('Fool of a Took', $modified); + } + + #[Test] + public function it_resolves_an_array(): void + { + $modified = $this->modify(['You shall not pass!', 'Fool of a Took']); + $this->assertIsArray($modified); + $this->assertEquals(['You shall not pass!', 'Fool of a Took'], $modified); + + $modified = $this->modify(['You shall not pass!', 'Fool of a Took'], [1]); + $this->assertEquals('Fool of a Took', $modified); + + $modified = $this->modify(['one' => 'You shall not pass!', 'two' => 'Fool of a Took'], ['two']); + $this->assertEquals('Fool of a Took', $modified); + } + + private function modify($value, array $params = []) + { + return Modify::value($value)->resolve($params)->fetch(); + } +} From fd6f832bb17b3b0b1ac3adc2eb177610b9d0f5fa Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 8 Jul 2025 10:52:58 -0400 Subject: [PATCH 271/490] [5.x] Fix active state for nav items with implicit children (#11937) --- src/CP/Navigation/NavItem.php | 9 +++++++++ tests/CP/Navigation/ActiveNavItemTest.php | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/CP/Navigation/NavItem.php b/src/CP/Navigation/NavItem.php index 7e860638293..dfb95673911 100644 --- a/src/CP/Navigation/NavItem.php +++ b/src/CP/Navigation/NavItem.php @@ -292,6 +292,14 @@ protected function currentUrlIsRestfulDescendant(): bool ]); } + /** + * Check if we should assume nested URL conventions for active state on children. + */ + protected function doesntHaveExplicitChildren(): bool + { + return (bool) ! $this->children; + } + /** * Check if this nav item was ever a child before user preferences were applied. */ @@ -393,6 +401,7 @@ public function isActive() if ($this->currentUrlIsNotExplicitlyReferencedInNav()) { switch (true) { case $this->currentUrlIsRestfulDescendant(): + case $this->doesntHaveExplicitChildren(): case $this->wasOriginallyChild(): return $this->isActiveByPattern($this->active); } diff --git a/tests/CP/Navigation/ActiveNavItemTest.php b/tests/CP/Navigation/ActiveNavItemTest.php index ab560f7ddf1..d59d9f4ac0d 100644 --- a/tests/CP/Navigation/ActiveNavItemTest.php +++ b/tests/CP/Navigation/ActiveNavItemTest.php @@ -536,6 +536,25 @@ public function it_properly_handles_various_edge_cases_when_checking_is_active_o $this->assertFalse($externalSecure->isActive()); } + #[Test] + public function active_nav_descendant_url_still_functions_properly_when_parent_item_has_no_children() + { + Facades\CP\Nav::extend(function ($nav) { + $nav->tools('Schopify')->url('/cp/totally-custom-url'); + }); + + $this + ->prepareNavCaches() + ->get('http://localhost/cp/totally-custom-url/deeper/descendant') + ->assertStatus(200); + + $toolsItems = $this->build()->get('Tools'); + + $this->assertTrue($this->getItemByDisplay($toolsItems, 'Schopify')->isActive()); + $this->assertFalse($this->getItemByDisplay($toolsItems, 'Addons')->isActive()); + $this->assertFalse($this->getItemByDisplay($toolsItems, 'Utilities')->isActive()); + } + #[Test] public function active_nav_check_still_functions_properly_when_custom_nav_extension_hijacks_a_core_item_child() { From 8fa8661ee0a98b3223980149ebeae169c0814d1e Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 8 Jul 2025 11:18:58 -0400 Subject: [PATCH 272/490] [5.x] Fix issue around spaces in git paths (#11933) --- src/Git/Git.php | 13 ++++++++- tests/Git/GitTest.php | 61 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/Git/Git.php b/src/Git/Git.php index 5dad7536808..073a5a42342 100644 --- a/src/Git/Git.php +++ b/src/Git/Git.php @@ -3,6 +3,7 @@ namespace Statamic\Git; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Collection; use Statamic\Console\Processes\Git as GitProcess; use Statamic\Contracts\Auth\User as UserContract; use Statamic\Facades\Antlers; @@ -255,7 +256,7 @@ protected function getCommandContext($paths, $message) { return [ 'git' => config('statamic.git.binary'), - 'paths' => collect($paths)->implode(' '), + 'paths' => $this->shellQuotePaths($paths), 'message' => $this->shellEscape($message), 'name' => $this->shellEscape($this->gitUserName()), 'email' => $this->shellEscape($this->gitUserEmail()), @@ -282,4 +283,14 @@ protected function shellEscape(string $string) return escapeshellcmd($string); } + + /** + * Shell quote paths to a string for use in git commands. + */ + protected function shellQuotePaths(Collection $paths): string + { + return collect($paths) + ->map(fn ($path) => '"'.$path.'"') + ->implode(' '); + } } diff --git a/tests/Git/GitTest.php b/tests/Git/GitTest.php index a06d6bbac28..498856f3338 100644 --- a/tests/Git/GitTest.php +++ b/tests/Git/GitTest.php @@ -19,7 +19,7 @@ class GitTest extends TestCase private $files; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -266,6 +266,65 @@ public function it_shell_escapes_git_user_name_and_email() $this->assertStringContainsString($expectedMessage, $lastCommit); } + #[Test] + public function it_commits_with_spaces_in_paths() + { + $this->files->put(base_path('content/collections/file with spaces.yaml'), 'title: File with spaces in path!'); + $this->files->makeDirectory(base_path('content/collections/folder with spaces')); + $this->files->put(base_path('content/collections/folder with spaces/file.yaml'), 'title: Folder with spaces in path!'); + + $expectedContentStatus = <<<'EOT' +?? "collections/file with spaces.yaml" +?? "collections/folder with spaces/" +EOT; + + $this->assertEquals($expectedContentStatus, GitProcess::create(Path::resolve(base_path('content')))->status()); + + $this->assertStringContainsString('Initial commit.', $this->showLastCommit(base_path('content'))); + + Git::commit(); + + $this->assertStringContainsString('Content saved', $commit = $this->showLastCommit(base_path('content'))); + $this->assertStringContainsString('Spock ', $commit); + $this->assertStringContainsString('collections/file with spaces.yaml', $commit); + $this->assertStringContainsString('title: File with spaces in path!', $commit); + $this->assertStringContainsString('collections/folder with spaces/file.yaml', $commit); + $this->assertStringContainsString('title: Folder with spaces in path!', $commit); + } + + #[Test] + public function it_commits_with_spaces_in_explicitly_configured_paths() + { + Config::set('statamic.git.paths', [ + 'content/path with spaces', + ]); + + $this->files->makeDirectory(base_path('content/path with spaces')); + $this->files->put(base_path('content/path with spaces/file.yaml'), 'title: File with spaces in path!'); + $this->files->put(base_path('content/path with spaces/nested file with spaces.yaml'), 'title: Nested file with spaces in path!'); + $this->files->makeDirectory(base_path('content/path with spaces/nested folder with spaces')); + $this->files->put(base_path('content/path with spaces/nested folder with spaces/file.yaml'), 'title: Nested folder with spaces in path!'); + + $expectedStatus = <<<'EOT' +?? "path with spaces/" +EOT; + + $this->assertEquals($expectedStatus, GitProcess::create(Path::resolve(base_path('content')))->status()); + + $this->assertStringContainsString('Initial commit.', $this->showLastCommit(base_path('content'))); + + Git::commit(); + + $this->assertStringContainsString('Content saved', $commit = $this->showLastCommit(base_path('content'))); + $this->assertStringContainsString('Spock ', $commit); + $this->assertStringContainsString('path with spaces/file.yaml', $commit); + $this->assertStringContainsString('title: File with spaces in path!', $commit); + $this->assertStringContainsString('path with spaces/nested file with spaces.yaml', $commit); + $this->assertStringContainsString('title: Nested file with spaces in path!', $commit); + $this->assertStringContainsString('path with spaces/nested folder with spaces/file.yaml', $commit); + $this->assertStringContainsString('title: Nested folder with spaces in path!', $commit); + } + #[Test] public function it_can_commit_with_custom_commit_message() { From 13f3fcbad40bbeb23459d5f8240f008b4b3af5fc Mon Sep 17 00:00:00 2001 From: Bram de Leeuw Date: Tue, 8 Jul 2025 17:19:18 +0200 Subject: [PATCH 273/490] [5.x] Check if sometimes rule is present before adding nonNull type (#11917) Co-authored-by: Jason Varga --- src/Fields/Field.php | 7 ++++++- tests/Fields/FieldTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Fields/Field.php b/src/Fields/Field.php index ef9c1c9a36a..e40cbe2128f 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -177,6 +177,11 @@ public function isRequired() return collect($this->rules()[$this->handle])->contains('required'); } + private function hasSometimesRule() + { + return collect($this->rules()[$this->handle])->contains('sometimes'); + } + public function setValidationContext($context) { $this->validationContext = $context; @@ -437,7 +442,7 @@ public function toGql(): array $type = ['type' => $type]; } - if ($this->isRequired()) { + if ($this->isRequired() && ! $this->hasSometimesRule()) { $type['type'] = GraphQL::nonNull($type['type']); } diff --git a/tests/Fields/FieldTest.php b/tests/Fields/FieldTest.php index 62c92217484..7c685b705cb 100644 --- a/tests/Fields/FieldTest.php +++ b/tests/Fields/FieldTest.php @@ -605,6 +605,31 @@ public function toGqlType() $this->assertInstanceOf(\GraphQL\Type\Definition\FloatType::class, $type['type']->getWrappedType()); } + #[Test] + #[Group('graphql')] + public function it_keeps_the_graphql_type_nullable_if_its_sometimes_required() + { + $fieldtype = new class extends Fieldtype + { + public function toGqlType() + { + return new \GraphQL\Type\Definition\FloatType; + } + }; + + FieldtypeRepository::shouldReceive('find') + ->with('fieldtype') + ->andReturn($fieldtype); + + $field = new Field('test', ['type' => 'fieldtype', 'validate' => 'required|sometimes']); + + $type = $field->toGql(); + + $this->assertIsArray($type); + $this->assertInstanceOf(\GraphQL\Type\Definition\NullableType::class, $type['type']); + $this->assertInstanceOf(\GraphQL\Type\Definition\FloatType::class, $type['type']); + } + #[Test] public function it_gets_the_path_of_handles_for_nested_fields() { From 426ae8abe2fb4b212d5c9442ef07a99c886102be Mon Sep 17 00:00:00 2001 From: Cas Ebbers <617080+CasEbb@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:28:32 +0200 Subject: [PATCH 274/490] [5.x] Use `app.locale` as fallback when there is no explicit site locale (#11939) --- src/Sites/Sites.php | 2 +- tests/Sites/SitesConfigTest.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Sites/Sites.php b/src/Sites/Sites.php index 717ed3ad7f2..a93277ea66f 100644 --- a/src/Sites/Sites.php +++ b/src/Sites/Sites.php @@ -138,7 +138,7 @@ protected function getFallbackConfig() 'default' => [ 'name' => '{{ config:app:name }}', 'url' => '/', - 'locale' => 'en_US', + 'locale' => '{{ config:app:locale }}', ], ]; } diff --git a/tests/Sites/SitesConfigTest.php b/tests/Sites/SitesConfigTest.php index dfa0d4ac134..27317f2440a 100644 --- a/tests/Sites/SitesConfigTest.php +++ b/tests/Sites/SitesConfigTest.php @@ -73,12 +73,11 @@ public function it_gets_default_site_without_yaml() Site::swap(new Sites); $this->assertCount(1, Site::all()); - $this->assertSame('default', Site::default()->handle()); $this->assertSame(config('app.name'), Site::default()->name()); $this->assertSame('/', Site::default()->url()); - $this->assertSame('en_US', Site::default()->locale()); - $this->assertSame('en', Site::default()->lang()); + $this->assertSame(config('app.locale'), Site::default()->locale()); + $this->assertSame(config('app.locale'), Site::default()->lang()); } #[Test] From 2ef8bac624f2b170c2c1f51cd54f918df1857e2b Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Thu, 10 Jul 2025 22:20:15 +0200 Subject: [PATCH 275/490] [5.x] Add optional fallback for missing keys in `{{ trans }}` tag (#11944) --- src/Tags/Trans.php | 8 +++++++- tests/Tags/TransTagTest.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Tags/Trans.php b/src/Tags/Trans.php index 9e560bbe6d0..0738e7b2063 100644 --- a/src/Tags/Trans.php +++ b/src/Tags/Trans.php @@ -13,8 +13,14 @@ public function wildcard($tag) { $key = $this->params->get('key', $tag); $locale = $this->params->pull('locale') ?? $this->params->pull('site'); + $fallback = $this->params->get('fallback'); $params = $this->params->all(); - return __($key, $params, $locale); + $translation = __($key, $params, $locale); + if ($fallback && $translation === $key) { + return __($fallback, $params, $locale); + } else { + return $translation; + } } } diff --git a/tests/Tags/TransTagTest.php b/tests/Tags/TransTagTest.php index 27e6a3518dd..0887955d322 100644 --- a/tests/Tags/TransTagTest.php +++ b/tests/Tags/TransTagTest.php @@ -38,4 +38,18 @@ public function it_translates_to_specific_locale() $this->assertEquals('Bonjour, Bob', $this->parse('{{ trans key="package::messages.hello_name" name="Bob" locale="fr" }}')); $this->assertEquals('Bonjour, Bob', $this->parse('{{ trans key="package::messages.hello_name" name="Bob" site="fr" }}')); } + + #[Test] + public function it_falls_back_to_fallback_for_missing_key() + { + $this->assertEquals('Fallback', $this->parse('{{ trans key="package::messages.does_not_exist" fallback="Fallback" }}')); + $this->assertEquals('Bonjour', $this->parse('{{ trans key="package::messages.does_not_exist" fallback="package::messages.hello" locale="fr" }}')); + } + + #[Test] + public function it_applies_replacement_to_fallbacks() + { + $this->assertEquals('Fallback Bob', $this->parse('{{ trans key="package::messages.does_not_exist" name="Bob" fallback="Fallback :name" }}')); + $this->assertEquals('Bonjour, Bob', $this->parse('{{ trans key="package::messages.does_not_exist" name="Bob" fallback="package::messages.hello_name" locale="fr" }}')); + } } From f38ad40d2c042de1be2b7dc8a497ae6c73ee2221 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 10 Jul 2025 16:46:03 -0400 Subject: [PATCH 276/490] changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6d0663c59..a515641aab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Release Notes +## 5.59.0 (2025-07-10) + +### What's new +- Add optional fallback for missing keys in `{{ trans }}` tag [#11944](https://github.com/statamic/cms/issues/11944) by @daun +- `resolve` modifier [#11890](https://github.com/statamic/cms/issues/11890) by @marcorieser +- Allow fetching children of other entries in `{{ children }}` tag [#11922](https://github.com/statamic/cms/issues/11922) by @daun +- Prompt to update search indexes when installing starter kits [#11924](https://github.com/statamic/cms/issues/11924) by @jesseleite + +### What's fixed +- Use `app.locale` as fallback when there is no explicit site locale [#11939](https://github.com/statamic/cms/issues/11939) by @CasEbb +- Check if sometimes rule is present before adding nonNull type [#11917](https://github.com/statamic/cms/issues/11917) by @TheBnl +- Fix issue around spaces in git paths [#11933](https://github.com/statamic/cms/issues/11933) by @jesseleite +- Fix active state for nav items with implicit children [#11937](https://github.com/statamic/cms/issues/11937) by @jesseleite +- Fix visibility of custom nav items for users w/ limited permissions [#11930](https://github.com/statamic/cms/issues/11930) by @jesseleite +- Fix group fieldtype child field validation rules when using {this} within a replicator/grid [#11931](https://github.com/statamic/cms/issues/11931) by @martyf +- Relax strict type check in `Tree::move()` [#11927](https://github.com/statamic/cms/issues/11927) by @duncanmcclean +- Prevent group fieldtype from filtering out `false` values [#11928](https://github.com/statamic/cms/issues/11928) by @duncanmcclean +- Class "DB" not found issue [#11911](https://github.com/statamic/cms/issues/11911) by @leganz +- Fix casing on dropdown item [#11907](https://github.com/statamic/cms/issues/11907) by @duncanmcclean +- German translations [#11903](https://github.com/statamic/cms/issues/11903) by @helloDanuk + + + ## 5.58.1 (2025-06-25) ### What's fixed From f585abd519083f07c9115c68e044cb4394f9e12a Mon Sep 17 00:00:00 2001 From: John Koster Date: Mon, 14 Jul 2025 10:08:24 -0500 Subject: [PATCH 277/490] [5.x] Allow classes to extend Markdown Parser (#11946) --- src/Markdown/Parser.php | 2 +- tests/Markdown/ParserTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Markdown/Parser.php b/src/Markdown/Parser.php index 93752d40ad2..9c4a9aa7605 100644 --- a/src/Markdown/Parser.php +++ b/src/Markdown/Parser.php @@ -185,7 +185,7 @@ public function config($key = null) public function newInstance(array $config = []) { - $parser = new self(array_replace_recursive($this->config, $config)); + $parser = new static(array_replace_recursive($this->config, $config)); foreach ($this->extensions as $ext) { $parser->addExtensions($ext); diff --git a/tests/Markdown/ParserTest.php b/tests/Markdown/ParserTest.php index 8e13247b388..3003e08748a 100644 --- a/tests/Markdown/ParserTest.php +++ b/tests/Markdown/ParserTest.php @@ -5,6 +5,8 @@ use League\CommonMark\Extension\CommonMark\Node\Block\Heading; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use PHPUnit\Framework\Attributes\Test; +use Statamic\Fields\Field; +use Statamic\Fields\Value; use Statamic\Markdown; use Tests\TestCase; @@ -128,4 +130,30 @@ public function it_creates_a_new_instance_based_on_the_current_instance() $this->assertCount(2, $newParser->renderers()); $this->assertCount(1, $this->parser->renderers()); } + + #[Test] + public function it_returns_instances_of_custom_parsers() + { + $markdown = new \Statamic\Fieldtypes\Markdown; + $field = new Field('test', [ + 'type' => 'markdown', + 'parser' => 'custom', + ]); + + $markdown->setField($field); + + $markdownValue = new Value('A Test', 'test', $markdown); + + $customParser = new class extends Markdown\Parser + { + public function parse(string $markdown): string + { + return strtolower($markdown); + } + }; + + \Statamic\Facades\Markdown::extend('custom', fn () => $customParser); + + $this->assertSame('a test', $markdownValue->value()); + } } From bb200c086824ce1cbbe908ba45c6e9e6b89aa5e7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 15 Jul 2025 14:55:11 +0100 Subject: [PATCH 278/490] [5.x] Licensing fixes (#11950) --- resources/views/partials/licensing-alerts.blade.php | 4 +++- src/Http/View/Composers/JavascriptComposer.php | 2 +- src/Licensing/LicenseManager.php | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/views/partials/licensing-alerts.blade.php b/resources/views/partials/licensing-alerts.blade.php index f5ff86034a4..8e8af0baa04 100644 --- a/resources/views/partials/licensing-alerts.blade.php +++ b/resources/views/partials/licensing-alerts.blade.php @@ -1,7 +1,9 @@ @php use function Statamic\trans as __; @endphp @inject('licenses', 'Statamic\Licensing\LicenseManager') -@if ($licenses->requestFailed()) +@if ($licenses->outpostIsOffline()) + {{-- Do nothing. --}} +@elseif ($licenses->requestFailed())
@if ($licenses->usingLicenseKeyFile()) diff --git a/src/Http/View/Composers/JavascriptComposer.php b/src/Http/View/Composers/JavascriptComposer.php index d9e0f253438..d1b4a0da29d 100644 --- a/src/Http/View/Composers/JavascriptComposer.php +++ b/src/Http/View/Composers/JavascriptComposer.php @@ -70,7 +70,7 @@ private function protectedVariables() 'preloadableFieldtypes' => FieldtypeRepository::preloadable()->keys(), 'livePreview' => config('statamic.live_preview'), 'permissions' => $this->permissions($user), - 'hasLicenseBanner' => $licenses->invalid() || $licenses->requestFailed(), + 'hasLicenseBanner' => ! $licenses->outpostIsOffline() && ($licenses->invalid() || $licenses->requestFailed()), 'customSvgIcons' => Icon::getCustomSvgIcons(), ]; } diff --git a/src/Licensing/LicenseManager.php b/src/Licensing/LicenseManager.php index aa879bec118..91c3d0ed2ed 100644 --- a/src/Licensing/LicenseManager.php +++ b/src/Licensing/LicenseManager.php @@ -44,6 +44,11 @@ public function requestValidationErrors() return new MessageBag($this->response('error') === 422 ? $this->response('errors') : []); } + public function outpostIsOffline() + { + return $this->requestErrorCode() >= 500 && $this->requestErrorCode() < 600; + } + public function isOnPublicDomain() { return $this->response('public'); From e865c35047372f89dc41f9cbe0b6f1641c0909c6 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 15 Jul 2025 17:19:28 +0100 Subject: [PATCH 279/490] [5.x] Addon Manifest improvements (#11948) Co-authored-by: Jason Varga --- src/Extend/Addon.php | 48 ++++++++++++++------------------- src/Extend/Manifest.php | 44 +++++++++++++++++++++++------- src/Marketplace/Client.php | 20 +++++++++----- src/Marketplace/Marketplace.php | 21 +++++---------- tests/Extend/AddonTest.php | 1 + 5 files changed, 76 insertions(+), 58 deletions(-) diff --git a/src/Extend/Addon.php b/src/Extend/Addon.php index 1b44943bb70..337c8782447 100644 --- a/src/Extend/Addon.php +++ b/src/Extend/Addon.php @@ -5,6 +5,7 @@ use Composer\Semver\VersionParser; use Facades\Statamic\Licensing\LicenseManager; use ReflectionClass; +use Statamic\Facades; use Statamic\Facades\File; use Statamic\Facades\Path; use Statamic\Support\Arr; @@ -147,6 +148,13 @@ final class Addon */ protected $editions = []; + /** + * Whether the addon has marketplace data. + * + * @var bool + */ + protected $hasMarketplaceData = false; + /** * @param string $id */ @@ -184,6 +192,10 @@ public static function makeFromPackage(array $package) $instance->$method($value); } + if (array_key_exists('marketplaceId', $package)) { + $instance->hasMarketplaceData = true; + } + return $instance; } @@ -231,32 +243,6 @@ public function vendorName() return explode('/', $this->package())[0]; } - /** - * The marketplace variant ID of the addon. - * - * @param int $id - * @return int - */ - public function marketplaceId($id = null) - { - return $id - ? $this->marketplaceId = $id - : $this->marketplaceId; - } - - /** - * The marketplace slug of the addon. - * - * @param string $slug - * @return string - */ - public function marketplaceSlug($slug = null) - { - return $slug - ? $this->marketplaceSlug = $slug - : $this->marketplaceSlug; - } - /** * The handle of the addon. * @@ -375,14 +361,14 @@ public function isLatestVersion() return true; } - if (! $this->latestVersion) { + if (! $this->latestVersion()) { return true; } $versionParser = new VersionParser; $version = $versionParser->normalize($this->version); - $latestVersion = $versionParser->normalize($this->latestVersion); + $latestVersion = $versionParser->normalize($this->latestVersion()); return version_compare($version, $latestVersion, '='); } @@ -407,6 +393,12 @@ public function __call($method, $args) } if (empty($args)) { + if (! $this->hasMarketplaceData && in_array($method, ['marketplaceId', 'marketplaceSlug', 'marketplaceUrl', 'marketplaceSellerSlug', 'isCommercial', 'latestVersion'])) { + app(Manifest::class)->fetchPackageDataFromMarketplace(); + + return Facades\Addon::get($this->id)->$method(); + } + return $this->$method; } diff --git a/src/Extend/Manifest.php b/src/Extend/Manifest.php index 9be9273034d..f9e57664915 100644 --- a/src/Extend/Manifest.php +++ b/src/Extend/Manifest.php @@ -4,6 +4,7 @@ use Facades\Statamic\Marketplace\Marketplace; use Illuminate\Foundation\PackageManifest; +use Illuminate\Support\Facades\Facade; use ReflectionClass; use Statamic\Facades\File; use Statamic\Support\Arr; @@ -48,21 +49,12 @@ protected function formatPackage($package) $statamic = $json['extra']['statamic'] ?? []; $author = $json['authors'][0] ?? null; - $edition = config('statamic.editions.addons.'.$package['name']); - - $marketplaceData = Marketplace::package($package['name'], $package['version'], $edition); - return [ 'id' => $package['name'], 'slug' => $statamic['slug'] ?? null, 'editions' => $statamic['editions'] ?? [], - 'marketplaceId' => data_get($marketplaceData, 'id', null), - 'marketplaceSlug' => data_get($marketplaceData, 'slug', null), - 'marketplaceUrl' => data_get($marketplaceData, 'url', null), - 'marketplaceSellerSlug' => data_get($marketplaceData, 'seller', null), - 'isCommercial' => data_get($marketplaceData, 'is_commercial', false), - 'latestVersion' => data_get($marketplaceData, 'latest_version', null), 'version' => Str::removeLeft($package['version'], 'v'), + 'raw_version' => $package['version'], 'namespace' => $namespace, 'autoload' => $autoload, 'provider' => $provider, @@ -81,4 +73,36 @@ public function addons() { return collect($this->getManifest()); } + + public function fetchPackageDataFromMarketplace() + { + $packages = $this->addons() + ->map(function (array $package) { + return [ + 'package' => $package['id'], + 'version' => $package['raw_version'], + 'edition' => config('statamic.editions.addons.'.$package['id']), + ]; + }) + ->values() + ->all(); + + $marketplaceData = Marketplace::packages($packages); + + $this->write($this->manifest = $this->addons()->map(function (array $package) use ($marketplaceData) { + $marketplaceData = $marketplaceData->get($package['id']); + + return [ + ...$package, + 'marketplaceId' => data_get($marketplaceData, 'id'), + 'marketplaceSlug' => data_get($marketplaceData, 'slug'), + 'marketplaceUrl' => data_get($marketplaceData, 'url'), + 'marketplaceSellerSlug' => data_get($marketplaceData, 'seller'), + 'isCommercial' => data_get($marketplaceData, 'is_commercial', false), + 'latestVersion' => data_get($marketplaceData, 'latest_version'), + ]; + })->all()); + + Facade::clearResolvedInstance(AddonRepository::class); + } } diff --git a/src/Marketplace/Client.php b/src/Marketplace/Client.php index 23227172488..c58c8ba6c8c 100644 --- a/src/Marketplace/Client.php +++ b/src/Marketplace/Client.php @@ -44,14 +44,22 @@ public function __construct() } } + public function get(string $endpoint, array $params = []) + { + return $this->request('GET', $endpoint, $params); + } + + public function post(string $endpoint, array $params = []) + { + return $this->request('POST', $endpoint, $params); + } + /** * Send API request. * - * @param string $endpoint - * @param arra $params * @return mixed */ - public function get($endpoint, $params = []) + private function request(string $method, string $endpoint, array $params = []) { $lock = $this->lock(static::LOCK_KEY, 10); @@ -61,10 +69,10 @@ public function get($endpoint, $params = []) try { $lock->block(5); - return $this->cache()->rememberWithExpiration($key, function () use ($endpoint, $params) { - $response = Guzzle::request('GET', $endpoint, [ + return $this->cache()->rememberWithExpiration($key, function () use ($method, $endpoint, $params) { + $response = Guzzle::request($method, $endpoint, [ 'verify' => $this->verifySsl, - 'query' => $params, + ($method === 'GET' ? 'query' : 'json') => $params, ]); $json = json_decode($response->getBody(), true); diff --git a/src/Marketplace/Marketplace.php b/src/Marketplace/Marketplace.php index 98e586cd9b4..08f63425d94 100644 --- a/src/Marketplace/Marketplace.php +++ b/src/Marketplace/Marketplace.php @@ -11,25 +11,18 @@ class Marketplace { - public function package($package, $version = null, $edition = null) + public function packages(array $packages) { - $uri = "packages/$package/$version"; - - if ($edition) { - $uri .= "?edition=$edition"; - } - - return Cache::rememberWithExpiration("marketplace-$uri", function () use ($uri) { - $fallback = [5 => null]; + $uri = 'packages'; + $hash = md5(json_encode($packages)); + return Cache::rememberWithExpiration("marketplace-$uri-$hash", function () use ($uri, $packages) { try { - if (! $response = Client::get($uri)) { - return $fallback; - } + $response = Client::post($uri, ['packages' => $packages]); - return [60 => $response['data']]; + return [60 => collect($response['data'])]; } catch (RequestException $e) { - return $fallback; + return [5 => collect()]; } }); } diff --git a/tests/Extend/AddonTest.php b/tests/Extend/AddonTest.php index 5e5001a40f0..1b05010798d 100644 --- a/tests/Extend/AddonTest.php +++ b/tests/Extend/AddonTest.php @@ -279,6 +279,7 @@ private function makeFromPackage($attributes = []) 'developerUrl' => 'http://test-developer.com', 'version' => '1.0', 'editions' => ['foo', 'bar'], + 'marketplaceId' => null, ], $attributes)); } } From 9e25eaf7d4c7c77027568d0f375930829533f66e Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 15 Jul 2025 17:32:31 +0100 Subject: [PATCH 280/490] [5.x] Add command to clear asset_meta and asset_container_contents caches (#11960) --- src/Console/Commands/AssetsCacheClear.php | 55 +++++++++++++++++++++++ src/Providers/ConsoleServiceProvider.php | 1 + 2 files changed, 56 insertions(+) create mode 100644 src/Console/Commands/AssetsCacheClear.php diff --git a/src/Console/Commands/AssetsCacheClear.php b/src/Console/Commands/AssetsCacheClear.php new file mode 100644 index 00000000000..1c7e4cbf416 --- /dev/null +++ b/src/Console/Commands/AssetsCacheClear.php @@ -0,0 +1,55 @@ +has('cache.stores.asset_meta') && ! config()->has('cache.stores.asset_container_contents')) { + $this->components->error('You do not have any custom asset cache stores.'); + + return 0; + } + + if (config()->has('cache.stores.asset_meta')) { + spin(callback: fn () => Asset::make()->cacheStore()->flush(), message: 'Clearing the asset meta cache...'); + + $this->components->info('Your asset meta cache is now so very, very empty.'); + } + + if (config()->has('cache.stores.asset_container_contents')) { + spin(callback: fn () => (new AssetContainerContents)->cacheStore()->flush(), message: 'Clearing the asset folder cache...'); + + $this->components->info('Your asset folder cache is now so very, very empty.'); + } + } +} diff --git a/src/Providers/ConsoleServiceProvider.php b/src/Providers/ConsoleServiceProvider.php index 4a043e0ac46..c6f661288ca 100644 --- a/src/Providers/ConsoleServiceProvider.php +++ b/src/Providers/ConsoleServiceProvider.php @@ -11,6 +11,7 @@ class ConsoleServiceProvider extends ServiceProvider protected $commands = [ Commands\ListCommand::class, Commands\AddonsDiscover::class, + Commands\AssetsCacheClear::class, Commands\AssetsGeneratePresets::class, Commands\AssetsMeta::class, Commands\GlideClear::class, From 5468fab685ccc8f0b4a95e89405ee6c065d2237a Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 15 Jul 2025 12:44:33 -0400 Subject: [PATCH 281/490] changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a515641aab3..226ea50f10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Release Notes +## 5.60.0 (2025-07-15) + +### What's new +- Add command to clear asset_meta and asset_container_contents caches [#11960](https://github.com/statamic/cms/issues/11960) by @ryanmitchell + +### What's fixed +- Addon Manifest improvements [#11948](https://github.com/statamic/cms/issues/11948) by @duncanmcclean +- Licensing fixes [#11950](https://github.com/statamic/cms/issues/11950) by @duncanmcclean +- Allow classes to extend Markdown Parser [#11946](https://github.com/statamic/cms/issues/11946) by @JohnathonKoster + + + ## 5.59.0 (2025-07-10) ### What's new From d4203f3787e3f73711e616cd15bed0f577d2c056 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 22 Jul 2025 18:35:56 +0100 Subject: [PATCH 282/490] [5.x] Loosen up assertions in `ViteTest` (#11985) --- tests/Tags/ViteTest.php | 70 ++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/tests/Tags/ViteTest.php b/tests/Tags/ViteTest.php index 7c72ff832f7..cf642b00b6e 100644 --- a/tests/Tags/ViteTest.php +++ b/tests/Tags/ViteTest.php @@ -27,26 +27,23 @@ public function it_outputs_script() { $output = $this->tag('{{ vite src="test.js" }}'); - $expected = <<<'HTML' - - -HTML; + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.js" />', $output); - $this->assertEqualsIgnoringLineBreaks($expected, $output); + $this->assertStringContainsString('', $output); } #[Test] public function it_outputs_stylesheet() { - $output = $this->tag('{{ vite src="test.css" }}'); - $expected = <<<'HTML' - - -HTML; + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" />', $output); - $this->assertEqualsIgnoringLineBreaks($expected, $output); + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" />', $output); } #[Test] @@ -54,14 +51,17 @@ public function it_outputs_multiple_entry_points() { $output = $this->tag('{{ vite src="test.js|test.css" }}'); - $expected = <<<'HTML' - - - - -HTML; + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" />', $output); + + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.js" />', $output); - $this->assertEqualsIgnoringLineBreaks($expected, $output); + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" />', $output); + + $this->assertStringContainsString('', $output); } #[Test] @@ -69,14 +69,17 @@ public function it_includes_attributes() { $output = $this->tag('{{ vite src="test.js|test.css" alfa="bravo" attr:charlie="delta" }}'); - $expected = <<<'HTML' - - - - -HTML; + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" />', $output); + + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.js" />', $output); - $this->assertEqualsIgnoringLineBreaks($expected, $output); + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" charlie="delta" />', $output); + + $this->assertStringContainsString('', $output); } #[Test] @@ -84,14 +87,17 @@ public function it_includes_tag_specific_attributes() { $output = $this->tag('{{ vite src="test.js|test.css" alfa="bravo" attr:charlie="delta" attr:script:echo="foxtrot" attr:style:golf="hotel" }}'); - $expected = <<<'HTML' - - - - -HTML; + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" />', $output); + + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.js" />', $output); + + $this->assertStringContainsString('assertStringContainsString('href="http://localhost/build/assets/test-123.css" charlie="delta" golf="hotel" />', $output); - $this->assertEqualsIgnoringLineBreaks($expected, $output); + $this->assertStringContainsString('', $output); } // Ignore line breaks just for the sake of readability in the test. From 8ba786e13ecf6f606628722767fa5a1f90ec6183 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:42:18 -0400 Subject: [PATCH 283/490] [5.x] Bump form-data from 4.0.0 to 4.0.4 (#11979) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 183 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 168 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index da06be6bdcb..a807554d5f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4204,6 +4204,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4815,6 +4828,20 @@ "helper-js": "^1.3.7" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -4895,6 +4922,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -5278,12 +5350,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -5331,9 +5406,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/fuse.js": { "version": "7.0.0", @@ -5363,13 +5442,24 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5384,6 +5474,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5437,6 +5540,18 @@ "node": ">=4" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5447,6 +5562,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -5464,9 +5580,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5474,6 +5591,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/helper-js": { "version": "1.4.38", "resolved": "https://registry.npmjs.org/helper-js/-/helper-js-1.4.38.tgz", @@ -7831,6 +7975,15 @@ "resolved": "https://registry.npmjs.org/marked-plaintext/-/marked-plaintext-0.0.2.tgz", "integrity": "sha512-6u/EfbyqTV8p9CqxFUOK3RUGo1KGULVUaiacKRjb+9VzXpGYYQlgff76xjlfxOGYpsDkzYg+l28CtGd86I6c0w==" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", From c8c8b33cc550c0a0cb3342c1af3cebdbc8d53204 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Thu, 24 Jul 2025 10:05:21 -0400 Subject: [PATCH 284/490] [5.x] Fix AddonTestCase path for Windows (#11994) --- src/Testing/AddonTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testing/AddonTestCase.php b/src/Testing/AddonTestCase.php index 890960fe67d..a77cc1e47e2 100644 --- a/src/Testing/AddonTestCase.php +++ b/src/Testing/AddonTestCase.php @@ -27,7 +27,7 @@ protected function setUp(): void if (isset($uses[PreventsSavingStacheItemsToDisk::class])) { $reflection = new ReflectionClass($this); - $this->fakeStacheDirectory = Str::before(dirname($reflection->getFileName()), '/tests').'/tests/__fixtures__/dev-null'; + $this->fakeStacheDirectory = Str::before(dirname($reflection->getFileName()), DIRECTORY_SEPARATOR.'tests').'/tests/__fixtures__/dev-null'; $this->preventSavingStacheItemsToDisk(); } From 75b9f6e1a8c1de32f7b20ca0b6e1ada43572eec2 Mon Sep 17 00:00:00 2001 From: Mason Curry Date: Thu, 24 Jul 2025 09:28:53 -0500 Subject: [PATCH 285/490] [5.x] Allow static warm to use insecure by default with config key (#11978) --- config/static_caching.php | 5 ++++- src/Console/Commands/StaticWarm.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/static_caching.php b/config/static_caching.php index 3e4e5fe6e5a..758c7f633b9 100644 --- a/config/static_caching.php +++ b/config/static_caching.php @@ -150,7 +150,8 @@ |-------------------------------------------------------------------------- | | Here you may define the queue name and connection - | that will be used when warming the static cache. + | that will be used when warming the static cache and + | optionally set the "--insecure" flag by default. | */ @@ -158,6 +159,8 @@ 'warm_queue_connection' => env('STATAMIC_STATIC_WARM_QUEUE_CONNECTION'), + 'warm_insecure' => env('STATAMIC_STATIC_WARM_INSECURE', false), + /* |-------------------------------------------------------------------------- | Shared Error Pages diff --git a/src/Console/Commands/StaticWarm.php b/src/Console/Commands/StaticWarm.php index 55d45dd38ab..c64ea2f861b 100644 --- a/src/Console/Commands/StaticWarm.php +++ b/src/Console/Commands/StaticWarm.php @@ -257,7 +257,7 @@ private function exceedsMaxDepth($uri): bool private function shouldVerifySsl(): bool { - if ($this->option('insecure')) { + if ($this->option('insecure') || config('statamic.static_caching.warm_insecure')) { return false; } From cb3f46299f942f1be79d4055afc594c3c6e53621 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 25 Jul 2025 15:37:12 +0100 Subject: [PATCH 286/490] [5.x] Update `SECURITY.md` (#11996) --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 16f6b62596e..bf1e938ed1e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,7 +3,7 @@ If you discover a security vulnerability in Statamic, please review the followin ## Guidelines While working to identify potential security vulnerabilities in Statamic, we ask that you: -- **Privately** share any issues that you discover with us via statamic.com/support as soon as possible. +- **Privately** share any issues that you discover with us via support@statamic.com as soon as possible. - Give us a reasonable amount of time to address any reported issues before publicizing them. - Only report issues that are in scope. - Provide a quality report with precise explanations and concrete attack scenarios. From c4ebf3aaee267965d0fcf8b2caef0d2bb14a8e26 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 25 Jul 2025 13:57:05 -0400 Subject: [PATCH 287/490] [5.x] Escape redirect in user tag (#11999) --- src/Tags/Concerns/RendersForms.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tags/Concerns/RendersForms.php b/src/Tags/Concerns/RendersForms.php index 8f86e7edecb..b53e8d59ef6 100644 --- a/src/Tags/Concerns/RendersForms.php +++ b/src/Tags/Concerns/RendersForms.php @@ -104,7 +104,7 @@ protected function formMetaFields($meta) { return collect($meta) ->map(function ($value, $key) { - return sprintf('', $key, $value); + return sprintf('', $key, e($value)); }) ->implode("\n"); } From d97e46f420ed21861e6ffd723000708618566f90 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 25 Jul 2025 14:03:04 -0400 Subject: [PATCH 288/490] changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 226ea50f10a..d00990c797f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Release Notes +## 5.61.0 (2025-07-25) + +### What's new +- Allow static warm to use insecure by default with config key [#11978](https://github.com/statamic/cms/issues/11978) by @macaws + +### What's fixed +- Escape redirect in user tag [#11999](https://github.com/statamic/cms/issues/11999) by @jasonvarga +- Fix AddonTestCase path for Windows [#11994](https://github.com/statamic/cms/issues/11994) by @godismyjudge95 +- Bump form-data from 4.0.0 to 4.0.4 [#11979](https://github.com/statamic/cms/issues/11979) by @dependabot +- Loosen up assertions in `ViteTest` [#11985](https://github.com/statamic/cms/issues/11985) by @duncanmcclean +- Update security contact info [#11996](https://github.com/statamic/cms/issues/11996) by @duncanmcclean + + + ## 5.60.0 (2025-07-15) ### What's new From f6ac8427f1361446e4f0c6826d877b7a795a7035 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 31 Jul 2025 18:55:01 -0400 Subject: [PATCH 289/490] [5.x] Ability to explicitly disable text fieldtype focus (#12011) --- resources/js/components/fieldtypes/TextFieldtype.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/js/components/fieldtypes/TextFieldtype.vue b/resources/js/components/fieldtypes/TextFieldtype.vue index 67262ad5d09..dcff0a15386 100644 --- a/resources/js/components/fieldtypes/TextFieldtype.vue +++ b/resources/js/components/fieldtypes/TextFieldtype.vue @@ -3,7 +3,7 @@ ref="input" :value="value" :classes="config.classes" - :focus="config.focus || name === 'title' || name === 'alt'" + :focus="shouldFocus" :autocomplete="config.autocomplete" :autoselect="config.autoselect" :type="config.input_type" @@ -28,6 +28,16 @@ export default { mixins: [Fieldtype], + computed: { + shouldFocus() { + if (this.config.focus === false) { + return false; + } + + return this.config.focus || this.name === 'title' || this.name === 'alt'; + } + }, + methods: { inputUpdated(value) { if (! this.config.debounce) { From 50791dbefda6800ed8d581442f3357d7f53d85b5 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 4 Aug 2025 17:45:36 +0200 Subject: [PATCH 290/490] [5.x] Apply bottom padding to main nav (#12012) Apply bottom padding to main nav Signed-off-by: Philipp Daun --- resources/css/components/nav-main.css | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/resources/css/components/nav-main.css b/resources/css/components/nav-main.css index 66e2d63e9a4..77a573d4e28 100644 --- a/resources/css/components/nav-main.css +++ b/resources/css/components/nav-main.css @@ -3,10 +3,14 @@ ========================================================================== */ .nav-main { - @apply hidden select-none bg-white shadow h-screen absolute rtl:right-0 ltr:left-0 overflow-scroll w-56; + @apply hidden select-none bg-white shadow absolute rtl:right-0 ltr:left-0 overflow-scroll w-56; @apply dark:bg-dark-800 dark:shadow-dark; transition: all .3s; + height: calc(100dvh - 52px); + .showing-license-banner & { + height: calc(100dvh - 105px); + } h6 { @apply mt-6; } @@ -14,6 +18,10 @@ @apply list-none p-0 mt-0; } + ul:last-child { + @apply pb-8; + } + li { @apply p-0 text-sm; margin-top: 6px; @@ -70,10 +78,6 @@ @screen md { .nav-main { @apply fixed flex bg-transparent shadow-none overflow-auto rtl:border-l ltr:border-r dark:border-dark-900; - height: calc(100% - 52px); - .showing-license-banner & { - height: calc(100% - 105px); - } .nav-closed & { @apply border-0 shadow-none; From 26c9f7bf7401b58862d3bb17b8ea0ea7ea87cc65 Mon Sep 17 00:00:00 2001 From: James King Date: Mon, 4 Aug 2025 17:07:01 +0100 Subject: [PATCH 291/490] [5.x] Fix asset styling in link fieldtype (#12016) Co-authored-by: Jason Varga --- .../components/fieldtypes/LinkFieldtype.vue | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/resources/js/components/fieldtypes/LinkFieldtype.vue b/resources/js/components/fieldtypes/LinkFieldtype.vue index 10371423fde..63c5c8c95ae 100644 --- a/resources/js/components/fieldtypes/LinkFieldtype.vue +++ b/resources/js/components/fieldtypes/LinkFieldtype.vue @@ -36,21 +36,32 @@ /> - +
+ +
+ + ', + ]), $response->getContent()); - // The cached response should have the nocache placeholder, and the javascript. + // The cached response should be the same as the initial response. $this->assertTrue(file_exists($this->dir.'/about_.html')); $this->assertEquals(vsprintf('1 %s%s', [ $region->key(), @@ -148,13 +152,11 @@ public function it_should_add_the_javascript_if_there_is_a_csrf_token() ->get('/about') ->assertOk(); - // Initial response should be dynamic and not contain javascript. - $this->assertEquals(''.csrf_token().'', $response->getContent()); + // Initial response should have the placeholder and the javascript, NOT the real token. + $this->assertEquals('STATAMIC_CSRF_TOKEN', $response->getContent()); - // The cached response should have the token placeholder, and the javascript. + // The cached response should be the same as the initial response. $this->assertTrue(file_exists($this->dir.'/about_.html')); - $this->assertEquals(vsprintf('STATAMIC_CSRF_TOKEN%s', [ - '', - ]), file_get_contents($this->dir.'/about_.html')); + $this->assertEquals('STATAMIC_CSRF_TOKEN', file_get_contents($this->dir.'/about_.html')); } } From 22c3e02ad8b28a03becf1ca841cd3fe6cabbe9cd Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 18 Feb 2026 10:13:24 -0500 Subject: [PATCH 473/490] changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0062f30e70b..67e1ce83984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Release Notes +## 5.73.8 (2026-02-18) + +### What's fixed +- Avoid replacing nocache regions in initial full-measure response [#13953](https://github.com/statamic/cms/issues/13953) by @duncanmcclean +- Fix Icon fieldtype augment error when value is empty [#13966](https://github.com/statamic/cms/issues/13966) by @jhhazelaar +- Fix `whereIn()`/`whereNotIn()` error for booleans [#13952](https://github.com/statamic/cms/issues/13952) by @duncanmcclean + + + ## 5.73.7 (2026-02-13) ### What's fixed From 6c270dacc2be02bfc2eee500766f3309f59d47b3 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 18 Feb 2026 13:54:39 -0500 Subject: [PATCH 474/490] [5.x] Sanitize html in html fieldtype (#13992) --- package-lock.json | 17 +++++++++++++++++ package.json | 1 + .../js/components/fieldtypes/HtmlFieldtype.vue | 11 +++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e76e54d691..216fe84b45f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "body-scroll-lock": "^4.0.0-beta.0", "codemirror": "^5.58.2", "cookies-js": "^1.2.2", + "dompurify": "^3.3.1", "floating-vue": "^1.0.0-beta.19", "fuse.js": "^7.0.0", "highlight.js": "^11.7.0", @@ -3661,6 +3662,13 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -4803,6 +4811,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", diff --git a/package.json b/package.json index f986197fac0..f54210eeb90 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "body-scroll-lock": "^4.0.0-beta.0", "codemirror": "^5.58.2", "cookies-js": "^1.2.2", + "dompurify": "^3.3.1", "floating-vue": "^1.0.0-beta.19", "fuse.js": "^7.0.0", "highlight.js": "^11.7.0", diff --git a/resources/js/components/fieldtypes/HtmlFieldtype.vue b/resources/js/components/fieldtypes/HtmlFieldtype.vue index 9dbeebc35d0..252a84aeea0 100644 --- a/resources/js/components/fieldtypes/HtmlFieldtype.vue +++ b/resources/js/components/fieldtypes/HtmlFieldtype.vue @@ -1,9 +1,16 @@ From 3653440b6c341ce05ef8f090c46e0d9566dbe488 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 18 Feb 2026 19:17:48 +0000 Subject: [PATCH 475/490] [5.x] Correct test namespaces to avoid PSR-4 warnings (#13989) --- tests/Feature/Assets/DownloadAssetTest.php | 2 +- tests/Feature/Assets/ImageThumbnailTest.php | 2 +- tests/Feature/Assets/PdfThumbnailTest.php | 2 +- tests/Feature/Assets/SvgThumbnailTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Assets/DownloadAssetTest.php b/tests/Feature/Assets/DownloadAssetTest.php index 2346a45e58a..c61323c718b 100644 --- a/tests/Feature/Assets/DownloadAssetTest.php +++ b/tests/Feature/Assets/DownloadAssetTest.php @@ -1,6 +1,6 @@ Date: Wed, 18 Feb 2026 15:09:03 -0500 Subject: [PATCH 476/490] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e1ce83984..af34ce011ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Release Notes +## 5.73.9 (2026-02-18) + +### What's fixed +- Correct test namespaces to avoid PSR-4 warnings [#13989](https://github.com/statamic/cms/issues/13989) by @duncanmcclean +- Sanitize html in html fieldtype [#13992](https://github.com/statamic/cms/issues/13992) by @jasonvarga + + + ## 5.73.8 (2026-02-18) ### What's fixed From 10acda4eda266fe1af0480843eb845d982daba0b Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 19 Feb 2026 13:41:35 -0500 Subject: [PATCH 477/490] [5.x] Harden html rendering (#14006) --- resources/js/bootstrap/globals.js | 3 ++- resources/js/components/AddonDetails.vue | 3 ++- resources/js/components/GlobalSearch.vue | 2 +- resources/js/components/configure/Tabs.vue | 10 +++++++++- resources/js/components/data-list/DefaultField.vue | 9 +++++---- resources/js/components/fieldtypes/ColorFieldtype.vue | 2 +- .../js/components/fieldtypes/DictionaryFieldtype.vue | 11 +++++++++-- .../fieldtypes/DictionaryIndexFieldtype.vue | 7 ++++++- resources/js/components/publish/Field.vue | 3 ++- 9 files changed, 37 insertions(+), 13 deletions(-) diff --git a/resources/js/bootstrap/globals.js b/resources/js/bootstrap/globals.js index aca3362eb72..2f95f4bbe69 100644 --- a/resources/js/bootstrap/globals.js +++ b/resources/js/bootstrap/globals.js @@ -1,4 +1,5 @@ import { marked } from 'marked'; +import DOMPurify from 'dompurify'; import { translate, translateChoice } from '../translations/translator'; import uid from 'uniqid'; import PreviewHtml from '../components/fieldtypes/replicator/PreviewHtml'; @@ -68,7 +69,7 @@ export function tailwind_width_class(width) { } export function markdown(value) { - return marked(value); + return DOMPurify.sanitize(marked(value)); }; export function __(string, replacements) { diff --git a/resources/js/components/AddonDetails.vue b/resources/js/components/AddonDetails.vue index a455ae0eab8..fb486a139d8 100644 --- a/resources/js/components/AddonDetails.vue +++ b/resources/js/components/AddonDetails.vue @@ -50,6 +50,7 @@ + + From f8094c42bdba6d35be18d9e885c5110de7ef9771 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 25 Feb 2026 06:10:13 +0000 Subject: [PATCH 486/490] [5.x] Fix CSRF token on pages excluded from static caching (#14056) --- src/StaticCaching/Middleware/Cache.php | 5 ++ .../FullMeasureStaticCachingTest.php | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/StaticCaching/Middleware/Cache.php b/src/StaticCaching/Middleware/Cache.php index 86731fa0e3e..f80342957cc 100644 --- a/src/StaticCaching/Middleware/Cache.php +++ b/src/StaticCaching/Middleware/Cache.php @@ -13,6 +13,7 @@ use Statamic\Facades\StaticCache; use Statamic\Statamic; use Statamic\StaticCaching\Cacher; +use Statamic\StaticCaching\Cachers\AbstractCacher; use Statamic\StaticCaching\Cachers\ApplicationCacher; use Statamic\StaticCaching\Cachers\FileCacher; use Statamic\StaticCaching\Cachers\NullCacher; @@ -199,6 +200,10 @@ private function shouldBeCached($request, $response) return false; } + if ($this->cacher instanceof AbstractCacher && $this->cacher->isExcluded($this->cacher->getUrl($request))) { + return false; + } + return true; } diff --git a/tests/StaticCaching/FullMeasureStaticCachingTest.php b/tests/StaticCaching/FullMeasureStaticCachingTest.php index 6ff8e37092c..bf9ea6f432a 100644 --- a/tests/StaticCaching/FullMeasureStaticCachingTest.php +++ b/tests/StaticCaching/FullMeasureStaticCachingTest.php @@ -159,4 +159,72 @@ public function it_should_add_the_javascript_if_there_is_a_csrf_token() $this->assertTrue(file_exists($this->dir.'/about_.html')); $this->assertEquals('STATAMIC_CSRF_TOKEN', file_get_contents($this->dir.'/about_.html')); } + + #[Test] + public function excluded_pages_should_have_real_csrf_token() + { + config(['statamic.static_caching.exclude' => [ + 'urls' => ['/about'], + ]]); + + $this->withFakeViews(); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('default', '{{ csrf_token }}'); + + $this->createPage('about'); + + $response = $this + ->get('/about') + ->assertOk(); + + // The response should have the real CSRF token, not the placeholder. + $this->assertEquals(''.csrf_token().'', $response->getContent()); + + // The page should not be cached. + $this->assertFalse(file_exists($this->dir.'/about_.html')); + } + + #[Test] + public function excluded_pages_should_have_nocache_regions_replaced() + { + config(['statamic.static_caching.exclude' => [ + 'urls' => ['/about'], + ]]); + + app()->instance('example_count', 0); + + (new class extends \Statamic\Tags\Tags + { + public static $handle = 'example_count'; + + public function index() + { + $count = app('example_count'); + $count++; + app()->instance('example_count', $count); + + return $count; + } + })::register(); + + $this->withFakeViews(); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('default', '{{ example_count }} {{ nocache }}{{ example_count }}{{ /nocache }}'); + + $this->createPage('about'); + + StaticCache::nocacheJs('js here'); + StaticCache::nocachePlaceholder('Loading...'); + + $response = $this + ->get('/about') + ->assertOk(); + + // The response should have the nocache regions replaced with rendered content, no placeholders or JS. + $this->assertEquals('1 2', $response->getContent()); + $this->assertStringNotContainsString('