Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9d92554
when saving an entry, append the id the filename if there's a duplicate
jasonvarga May 7, 2021
bbf13df
Handle id suffixes when getting date from path
jasonvarga May 7, 2021
430a5b2
Class for getting slug from path, and handle suffixes
jasonvarga May 7, 2021
0c855ca
Adjust test so it actually does the thing it says
jasonvarga May 7, 2021
faa4062
Remove number prefixes which haven't been a thing for a while now, bu…
jasonvarga May 7, 2021
12c7e79
Remove Path::clean() and URL::buildFromPath() which are methods that …
jasonvarga May 7, 2021
01a564b
Apply fixes from StyleCI (#3670)
jasonvarga May 7, 2021
df88219
Merge branch '3.1' into feature/duplicate-slugs
jasonvarga May 10, 2021
9505b5c
use incrementing numbers rather than ids
jasonvarga May 10, 2021
4f1c932
Keep the suffix. It felt weird changing it, in practice.
jasonvarga May 10, 2021
e7fb873
Replace unique slug validation with unique uri validation
jasonvarga May 11, 2021
859e79d
Prevent error when validating entry in a collection without a route
jasonvarga May 11, 2021
4c95704
Deprecate findBySlug
jasonvarga May 11, 2021
a57d1e7
Ability to have placeholders and replacements in validation rules
jasonvarga May 11, 2021
dd9850a
Pass along replacements in entries
jasonvarga May 11, 2021
fe4c5af
add unique entry value rule to suggestions, and handle inserting a ru…
jasonvarga May 11, 2021
3b3499a
Merge branch '3.1' into feature/duplicate-slugs
jasonvarga May 21, 2021
f05e95d
Merge branch '3.1' into feature/duplicate-slugs
jasonvarga May 28, 2021
ac07846
Ability to disable the uri cache on a tree instance, so that $page->u…
jasonvarga Jun 3, 2021
c2aea41
Prevent submitting a tree if its going to result in a duplicate uri
jasonvarga Jun 3, 2021
047fc71
Merge branch 'feature/validation-replacements' into feature/duplicate…
jasonvarga Jun 3, 2021
bcaade1
Add unique slug validation to collection blueprints
jasonvarga Jun 3, 2021
fb14e97
silly php7.2
jasonvarga Jun 3, 2021
27bc98a
Translate
jasonvarga Jun 4, 2021
ee5dbf5
Save them first and flush the caches to be sure the most up to date o…
jasonvarga Jun 4, 2021
a4e6c83
Prevent duplicates if the command is run multiple times somehow
jasonvarga Jun 7, 2021
d5fa79b
Merge branch '3.1' into feature/duplicate-slugs
jasonvarga Jun 7, 2021
932986e
add other vars
jasonvarga Jun 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion resources/js/components/structures/PageTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,15 @@ export default {
this.initialPages = this.pages;
return response;
}).catch(e => {
this.$toast.error(e.response ? e.response.data.message : __('Something went wrong'));
let message = e.response ? e.response.data.message : __('Something went wrong');

// For a validation error, show the first message from any field in the toast.
if (e.response && e.response.status === 422) {
const { errors } = e.response.data;
message = errors[Object.keys(errors)[0]][0];
}

this.$toast.error(message);
return Promise.reject(e);
}).finally(() => this.saving = false);
},
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
'duplicate_field_handle' => 'Field with a handle of :handle cannot be used more than once.',
'one_site_without_origin' => 'At least one site must not have an origin.',
'origin_cannot_be_disabled' => 'Cannot select a disabled origin.',
'unique_uri' => 'This URI has already been taken.',
'duplicate_uri' => 'Duplicate URI :value',

/*
|--------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/Contracts/Entries/EntryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function find($id);

public function findByUri(string $uri);

/** @deprecated */
public function findBySlug(string $slug, string $collection);

public function make();
Expand Down
1 change: 1 addition & 0 deletions src/Contracts/Taxonomies/TermRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function find($id);

public function findByUri(string $uri);

/** @deprecated */
public function findBySlug(string $slug, string $collection);

public function make(string $slug = null);
Expand Down
9 changes: 7 additions & 2 deletions src/Data/ExistsAsFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ trait ExistsAsFile

abstract public function path();

public function buildPath()
{
return $this->path();
}

public function initialPath($path = null)
{
if (func_num_args() === 0) {
Expand Down Expand Up @@ -77,9 +82,9 @@ public function fileExtension()
return 'yaml';
}

public function writeFile()
public function writeFile($path = null)
{
$path = $this->path();
$path = $path ?? $this->buildPath();
$initial = $this->initialPath();

if ($initial && $path !== $initial) {
Expand Down
5 changes: 5 additions & 0 deletions src/Entries/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ public function taxonomize()
}

public function path()
{
return $this->initialPath ?? $this->buildPath();
}

public function buildPath()
{
$prefix = '';

Expand Down
15 changes: 12 additions & 3 deletions src/Entries/GetDateFromPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ public function __invoke($path)
{
$filename = pathinfo($path, PATHINFO_FILENAME);

return strpos($filename, '.') === false
? null
: explode('.', pathinfo($path, PATHINFO_FILENAME))[0];
if (strpos($filename, '.') === false) {
return null;
}

$firstSegment = explode('.', pathinfo($path, PATHINFO_FILENAME), 2)[0];

return $this->isDate($firstSegment) ? $firstSegment : null;
}

private function isDate($str)
{
return preg_match('/^\d{4}-\d{2}-\d{2}(-\d{4})?$/', $str);
}
}
28 changes: 28 additions & 0 deletions src/Entries/GetSlugFromPath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Statamic\Entries;

class GetSlugFromPath
{
public function __invoke($path)
{
$path = pathinfo($path, PATHINFO_FILENAME);

if (strpos($path, '.') === false) {
return $path;
}

$segments = explode('.', $path);

if ($this->isDate($segments[0])) {
return $segments[1];
}

return $segments[0];
}

private function isDate($str)
{
return preg_match('/^\d{4}-\d{2}-\d{2}(-\d{4})?$/', $str);
}
}
30 changes: 0 additions & 30 deletions src/Facades/Endpoint/Path.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,36 +89,6 @@ public function resolve($path)
return $leadingSlash ? Str::ensureLeft($path, '/') : $path;
}

/**
* Cleans up a given $path, removing any flags and order keys (date-based or number-based).
*
* Assumes the path will always end with an extension.
*
* @param string $path Path to clean
* @return string
*/
public function clean($path)
{
// Remove draft and hidden flags
$path = preg_replace('/\/_[_]?/', '/', $path);

// Strip the order keys
$segments = explode('/', $path);
$total_segments = count($segments);
foreach ($segments as $i => &$segment) {
// Skip the final segment (the basename) if it doesn't contain two periods.
// This stops filenames like 404.md from being interpreted with 404 as
// the order key, resulting in a borked filename.
if ($i + 1 === $total_segments && substr_count($segment, '.') < 2) {
continue;
}

$segment = preg_replace(Pattern::orderKey(), '', $segment);
}

return implode('/', $segments);
}

/**
* Assembles a URL from an ordered list of segments.
*
Expand Down
21 changes: 0 additions & 21 deletions src/Facades/Endpoint/URL.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,27 +245,6 @@ public function getSiteUrl()
return Str::ensureRight($rootUrl, '/');
}

/**
* Build a page URL from a path.
*
* @param string $path
* @return string
*/
public function buildFromPath($path)
{
$path = Path::makeRelative($path);

$ext = pathinfo($path)['extension'];

$path = Path::clean($path);

$path = preg_replace('/^pages/', '', $path);

$path = preg_replace('#\/(?:[a-z]+\.)?index\.'.$ext.'$#', '', $path);

return Str::ensureLeft($path, '/');
}

/**
* Encode a URL.
*
Expand Down
1 change: 0 additions & 1 deletion src/Facades/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* @method static \Statamic\Entries\EntryCollection whereInCollection(array $handles)
* @method static null|\Statamic\Contracts\Entries\Entry find($id)
* @method static null|\Statamic\Contracts\Entries\Entry findByUri(string $uri)
* @method static null|\Statamic\Contracts\Entries\Entry findBySlug(string $slug, string $collection)
* @method static \Statamic\Contracts\Entries\Entry make()
* @method static \Statamic\Contracts\Entries\QueryBuilder query()
* @method static void save($entry)
Expand Down
1 change: 0 additions & 1 deletion src/Facades/Term.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* @method static TermCollection whereInTaxonomy(array $handles)
* @method static TermContract find($id)
* @method static TermContract findByUri(string $uri, string $site = null)
* @method static TermContract findBySlug(string $slug, string $taxonomy)
* @method static save($term)
* @method static delete($term)
* @method static TermQueryBuilder query()
Expand Down
1 change: 0 additions & 1 deletion src/Facades/URL.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* @method static string format($url)
* @method static bool isExternal($url)
* @method static string getSiteUrl()
* @method static string buildFromPath($path)
* @method static string encode($url)
* @method static mixed getDefaultUri($locale, $uri)
* @method static string gravatar($email, $size = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Http\Controllers\CP\Collections;

use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Statamic\Facades\Entry;
use Statamic\Facades\User;
use Statamic\Http\Controllers\CP\CpController;
Expand All @@ -14,31 +15,22 @@ public function update(Request $request, $collection)
{
$this->authorize('reorder', $collection);

$deletedEntries = collect($request->deletedEntries ?? [])
->map(function ($id) {
return Entry::find($id);
})
->filter(function ($entry) {
return User::current()->can('delete', $entry);
});
$contents = $this->toTree($request->pages);

if ($request->deleteLocalizationBehavior === 'copy') {
$deletedEntries->each->detachLocalizations();
} else {
$deletedEntries->each->deleteDescendants();
}
$structure = $collection->structure();
$tree = $structure->in($request->site);

$deletedEntries->each->delete();
// Clone the tree and add the submitted contents into it so we can
// validate URI uniqueness without affecting the real object in memory.
$this->validateUniqueUris((clone $tree)->disableUriCache()->tree($contents));

$tree = $this->toTree($request->pages);
// Validate the tree, which will add any missing entries or throw an exception
// if somehow the root would end up having child pages, which isn't allowed.
$contents = $structure->validateTree($contents, $request->site);

$tree = $collection->structure()->validateTree($tree, $request->site);
$this->deleteEntries($request);

$collection
->structure()
->in($request->site)
->tree($tree)
->save();
$tree->tree($contents)->save();
}

protected function toTree($items)
Expand All @@ -52,4 +44,49 @@ protected function toTree($items)
]);
})->all();
}

private function validateUniqueUris($tree)
{
if (! $tree->collection()->route($tree->locale())) {
return;
}

foreach ($tree->diff()->moved() as $id) {
$page = $tree->page($id);
$parent = $page->parent();

$siblings = (! $parent || $parent->isRoot())
? $tree->pages()->all()->slice(1)
: $page->parent()->pages()->all();

$siblings = $siblings->reject(function ($sibling) use ($id) {
return $sibling->reference() === $id;
});

$uris = $siblings->map->uri();

if ($uris->contains($uri = $page->uri())) {
throw ValidationException::withMessages(['uri' => trans('statamic::validation.duplicate_uri', ['value' => $uri])]);
}
}
}

private function deleteEntries($request)
{
$deletedEntries = collect($request->deletedEntries ?? [])
->map(function ($id) {
return Entry::find($id);
})
->filter(function ($entry) {
return User::current()->can('delete', $entry);
});

if ($request->deleteLocalizationBehavior === 'copy') {
$deletedEntries->each->detachLocalizations();
} else {
$deletedEntries->each->deleteDescendants();
}

$deletedEntries->each->delete();
}
}
43 changes: 41 additions & 2 deletions src/Http/Controllers/CP/Collections/EntriesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Http\Controllers\CP\Collections;

use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Statamic\Contracts\Entries\Entry as EntryContract;
use Statamic\CP\Breadcrumbs;
use Statamic\Facades\Asset;
Expand Down Expand Up @@ -196,9 +197,9 @@ public function update(Request $request, $collection, $entry)
}

if ($collection->structure() && ! $collection->orderable()) {
$entry->afterSave(function ($entry) use ($parent) {
$tree = $entry->structure()->in($entry->locale());
$tree = $entry->structure()->in($entry->locale());

$entry->afterSave(function ($entry) use ($parent, $tree) {
if ($parent && optional($tree->page($parent))->isRoot()) {
$parent = null;
}
Expand All @@ -209,6 +210,8 @@ public function update(Request $request, $collection, $entry)
});
}

$this->validateUniqueUri($entry, $tree ?? null, $parent ?? null);

if ($entry->revisionsEnabled() && $entry->published()) {
$entry
->makeWorkingCopy()
Expand Down Expand Up @@ -340,6 +343,8 @@ public function store(Request $request, $collection, $site)
});
}

$this->validateUniqueUri($entry, $tree ?? null, $parent ?? null);

if ($entry->revisionsEnabled()) {
$entry->store([
'message' => $request->message,
Expand Down Expand Up @@ -432,6 +437,40 @@ protected function formatDateForSaving($date)
return $date;
}

private function validateUniqueUri($entry, $tree, $parent)
{
if (! $uri = $this->entryUri($entry, $tree, $parent)) {
return;
}

$existing = Entry::findByUri($uri);

if (! $existing || $existing->id() === $entry->id()) {
return;
}

throw ValidationException::withMessages(['slug' => __('statamic::validation.unique_uri')]);
}

private function entryUri($entry, $tree, $parent)
{
if (! $tree) {
return $entry->uri();
}

$parent = $parent ? $tree->page($parent) : null;

return app(\Statamic\Contracts\Routing\UrlBuilder::class)
->content($entry)
->merge([
'parent_uri' => $parent ? $parent->uri() : null,
'slug' => $entry->slug(),
// 'depth' => '', // todo
'is_root' => false,
])
->build($entry->route());
}

protected function breadcrumbs($collection)
{
return new Breadcrumbs([
Expand Down
Loading