Skip to content

Commit 2a2966f

Browse files
duncanmccleanclaudejasonvarga
authored
[6.x] Fix duplicate slugs allowed with depth-conditional routes (#14508)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent 5fc3f3f commit 2a2966f

3 files changed

Lines changed: 105 additions & 1 deletion

File tree

src/Http/Controllers/CP/Collections/EntriesController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,12 +486,16 @@ private function entryUri($entry, $tree, $parent)
486486

487487
$parent = $parent ? $tree->find($parent) : null;
488488

489+
if ($parent && $parent->isRoot()) {
490+
$parent = null;
491+
}
492+
489493
return app(\Statamic\Contracts\Routing\UrlBuilder::class)
490494
->content($entry)
491495
->merge([
492496
'parent_uri' => $parent ? $parent->uri() : null,
493497
'slug' => $entry->slug(),
494-
// 'depth' => '', // todo
498+
'depth' => $parent ? $parent->depth() + 1 : 1,
495499
'is_root' => false,
496500
])
497501
->build($entry->route());

tests/Feature/Entries/StoreEntryTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tests\Feature\Entries;
44

55
use Facades\Statamic\Fields\BlueprintRepository;
6+
use Facades\Tests\Factories\EntryFactory;
67
use Illuminate\Support\Facades\Event;
78
use PHPUnit\Framework\Attributes\DataProvider;
89
use PHPUnit\Framework\Attributes\Test;
@@ -343,6 +344,70 @@ public function user_without_publish_permission_gets_initial_published_false_eve
343344
$this->assertFalse($response->json('values.published'), 'Initial published value should be false when user lacks publish permission, even if collection defaults to published');
344345
}
345346

347+
#[Test]
348+
public function it_prevents_duplicate_uris_for_structured_entries_with_depth_conditional_routes()
349+
{
350+
$this->setTestRoles(['test' => ['access cp', 'create test entries']]);
351+
$user = tap(User::make()->assignRole('test'))->save();
352+
353+
$collection = tap(
354+
Collection::make('test')
355+
->routes('{{ if depth > 1 }}{{ parent_uri }}/{{ slug }}{{ else }}base/{{ slug }}{{ /if }}')
356+
->structureContents(['max_depth' => 10])
357+
)->save();
358+
359+
EntryFactory::id('root-id')->slug('root')->collection('test')->create();
360+
EntryFactory::id('child-id')->slug('child')->collection('test')->create();
361+
362+
$tree = $collection->structure()->in('en');
363+
$tree->tree([
364+
['entry' => 'root-id', 'children' => [
365+
['entry' => 'child-id'],
366+
]],
367+
])->save();
368+
369+
$this
370+
->actingAs($user)
371+
->submit($collection, [
372+
'title' => 'Duplicate Child',
373+
'slug' => 'child',
374+
'_parent' => 'root-id',
375+
])
376+
->assertStatus(422)
377+
->assertJsonValidationErrors(['slug']);
378+
}
379+
380+
#[Test]
381+
public function it_prevents_duplicate_uris_when_parent_is_the_explicit_root()
382+
{
383+
$this->setTestRoles(['test' => ['access cp', 'create test entries']]);
384+
$user = tap(User::make()->assignRole('test'))->save();
385+
386+
$collection = tap(
387+
Collection::make('test')
388+
->routes('{{ if depth > 1 }}{{ parent_uri }}/{{ slug }}{{ else }}base/{{ slug }}{{ /if }}')
389+
->structureContents(['root' => true, 'max_depth' => 10])
390+
)->save();
391+
392+
EntryFactory::id('root-id')->slug('root')->collection('test')->create();
393+
EntryFactory::id('sibling-id')->slug('sibling')->collection('test')->create();
394+
395+
$collection->structure()->in('en')->tree([
396+
['entry' => 'root-id'],
397+
['entry' => 'sibling-id'],
398+
])->save();
399+
400+
$this
401+
->actingAs($user)
402+
->submit($collection, [
403+
'title' => 'Duplicate Sibling',
404+
'slug' => 'sibling',
405+
'_parent' => 'root-id',
406+
])
407+
->assertStatus(422)
408+
->assertJsonValidationErrors(['slug']);
409+
}
410+
346411
private function seedUserAndCollection()
347412
{
348413
$this->setTestRoles(['test' => ['access cp', 'create test entries']]);

tests/Feature/Entries/UpdateEntryTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,41 @@ public function does_not_validate_max_depth_when_collection_max_depth_is_null()
469469
->assertOk();
470470
}
471471

472+
#[Test]
473+
public function it_prevents_duplicate_uris_for_structured_entries_with_depth_conditional_routes()
474+
{
475+
$this->setTestRoles(['test' => ['access cp', 'edit test entries', 'access en site']]);
476+
$user = tap(User::make()->assignRole('test'))->save();
477+
478+
$collection = tap(
479+
Collection::make('test')
480+
->routes('{{ if depth > 1 }}{{ parent_uri }}/{{ slug }}{{ else }}base/{{ slug }}{{ /if }}')
481+
->structureContents(['max_depth' => 10])
482+
)->save();
483+
484+
EntryFactory::id('root-id')->slug('root')->collection('test')->create();
485+
EntryFactory::id('child-id')->slug('child')->collection('test')->create();
486+
487+
$entry = EntryFactory::id('other-child-id')
488+
->slug('other-child')
489+
->collection('test')
490+
->data(['title' => 'Other Child'])
491+
->create();
492+
493+
$collection->structure()->in('en')->tree([
494+
['entry' => 'root-id', 'children' => [
495+
['entry' => 'child-id'],
496+
['entry' => 'other-child-id'],
497+
]],
498+
])->save();
499+
500+
$this
501+
->actingAs($user)
502+
->update($entry, ['title' => 'Other Child', 'slug' => 'child'])
503+
->assertStatus(422)
504+
->assertJsonValidationErrors(['slug']);
505+
}
506+
472507
private function seedUserAndCollection()
473508
{
474509
$this->setTestRoles(['test' => [

0 commit comments

Comments
 (0)