Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a90af0b
Prototype
jaygeorge Aug 24, 2026
2f8cf26
Adjust combobox size
jaygeorge Aug 24, 2026
0344bcf
Correct alignment
jaygeorge Aug 24, 2026
cc0eb9e
Improve layout
jaygeorge Aug 24, 2026
f2ad7dc
When selecting a value in the combobox, the toggle should turn on, ot…
jaygeorge Aug 24, 2026
e56050c
Don't say "Sites" Twice
jaygeorge Aug 24, 2026
3a9eb2b
Add translation
jaygeorge Aug 24, 2026
d9ef435
Prevent circular origin loops from hanging the CP, e.g. if Tokyo > Ja…
jaygeorge Aug 24, 2026
58a9e54
Fix site grouping PHPUnit failures
jaygeorge Aug 24, 2026
4c504e7
Merge branch 'multisite-entry-grouping' into multisite-globals-selection
jaygeorge Aug 24, 2026
fa6f0e5
Merge branch '6.x' into multisite-globals-selection
jaygeorge Aug 24, 2026
ed0b2e2
Add origin_handle to entry revision localizations
jaygeorge Aug 24, 2026
a66f7dc
Do not translate user-defined site group names
jaygeorge Aug 24, 2026
2a4aa03
Remove unused Site::filterByGroup helper
jaygeorge Aug 24, 2026
ec3633e
Reject circular entry origins on save
jaygeorge Aug 24, 2026
30c7845
Add tests for circular origin guards
jaygeorge Aug 24, 2026
99e749e
Drop non-English Localization translations
jaygeorge Aug 24, 2026
9157cfd
Use existing Localizable label for globals sites section
jaygeorge Aug 24, 2026
f1d722e
Merge branch 'multisite-entry-grouping' into multisite-globals-selection
jaygeorge Aug 24, 2026
1a01ef6
Drop Socialite TestCase workaround again
jaygeorge Aug 24, 2026
074c1fa
Merge branch 'multisite-entry-grouping' into multisite-globals-selection
jaygeorge Aug 24, 2026
4267a43
Add checkbox selection to groups too
jaygeorge Aug 24, 2026
e3e190f
Fix CI failures for Log facade and Statamic\trans imports
jaygeorge Aug 24, 2026
687fce7
Avoid requiring id() for HasOrigin cycle guards
jaygeorge Aug 24, 2026
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
400 changes: 325 additions & 75 deletions resources/js/components/globals/Sites.vue

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/Auth/Protect/Protection.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Auth\Protect;

use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
use Statamic\Contracts\Auth\Protect\Protectable;
use Statamic\Facades\URL;
Expand Down Expand Up @@ -82,7 +83,7 @@ protected function url()

protected function log($message)
{
\Log::debug(vsprintf('%s Denying access to %s.', [
Log::debug(vsprintf('%s Denying access to %s.', [
$message,
$this->url(),
]));
Expand Down
92 changes: 84 additions & 8 deletions src/Data/HasOrigin.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,57 @@ trait HasOrigin
protected $origin;
private $cachedKeys;

public function keys()
public function keys(array $visited = [])
{
if ($this->cachedKeys) {
if (empty($visited) && $this->cachedKeys) {
return $this->cachedKeys;
}

$key = $this->originVisitKey();

if (in_array($key, $visited, true)) {
return collect();
}

$visited[] = $key;

$originFallbackKeys = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues()->keys() : collect();

$originKeys = $this->hasOrigin() ? $this->origin()->keys() : collect();
$originKeys = $this->hasOrigin() ? $this->origin()->keys($visited) : collect();

$computedKeys = method_exists($this, 'computedKeys') ? $this->computedKeys() : [];

return $this->cachedKeys = collect()
$keys = collect()
->merge($originFallbackKeys)
->merge($originKeys)
->merge($this->data->keys())
->merge($computedKeys);

if (count($visited) === 1) {
$this->cachedKeys = $keys;
}

return $keys;
}

public function values()
{
return $this->getValues(false);
}

public function getValues($wrapComputed)
public function getValues($wrapComputed, array $visited = [])
{
$key = $this->originVisitKey();

if (in_array($key, $visited, true)) {
return collect();
}

$visited[] = $key;

$originFallbackValues = method_exists($this, 'getOriginFallbackValues') ? $this->getOriginFallbackValues() : collect();

$originValues = $this->hasOrigin() ? $this->origin()->values() : collect();
$originValues = $this->hasOrigin() ? $this->origin()->getValues($wrapComputed, $visited) : collect();

$computedData = method_exists($this, 'getComputedData') ? $this->getComputedData($wrapComputed) : [];

Expand All @@ -51,11 +73,19 @@ public function getValues($wrapComputed)
->merge($computedData);
}

public function value($key)
public function value($key, array $visited = [])
{
$visitKey = $this->originVisitKey();

if (in_array($visitKey, $visited, true)) {
return null;
}

$visited[] = $visitKey;

$originFallbackValue = method_exists($this, 'getOriginFallbackValue') ? $this->getOriginFallbackValue($key) : null;

$originValue = $this->hasOrigin() ? $this->origin()->value($key) : $originFallbackValue;
$originValue = $this->hasOrigin() ? $this->origin()->value($key, $visited) : $originFallbackValue;

$value = $this->has($key) ? $this->get($key) : $originValue;

Expand Down Expand Up @@ -111,14 +141,60 @@ public function isRoot()
return ! $this->hasOrigin();
}

public function hasOriginCycle(): bool
{
$seen = [];
$entry = $this;

while ($entry->hasOrigin()) {
$key = $entry->originVisitKey();

if (isset($seen[$key])) {
return true;
}

$seen[$key] = true;

$entry = $entry->origin();

if (! $entry) {
break;
}
}

return false;
}

public function root()
{
$entry = $this;
$seen = [];

while ($entry->hasOrigin()) {
$key = $entry->originVisitKey();

if (isset($seen[$key])) {
break;
}

$seen[$key] = true;

$entry = $entry->origin();
}

return $entry;
}

protected function originVisitKey()
{
if (method_exists($this, 'id')) {
$id = $this->id();

if ($id !== null) {
return $id;
}
}

return 'object:'.spl_object_id($this);
}
}
20 changes: 20 additions & 0 deletions src/Entries/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Traits\Localizable;
use Illuminate\Validation\ValidationException;
use LogicException;
use Statamic\Contracts\Auth\Protect\Protectable;
use Statamic\Contracts\Data\Augmentable;
Expand Down Expand Up @@ -54,6 +55,8 @@
use Statamic\Support\Traits\FluentlyGetsAndSets;
use Statamic\View\Cascade;

use function Statamic\trans as __;

class Entry implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, ContainsQueryableValues, Contract, Localization, Protectable, ResolvesValuesContract, Responsable, SearchableContract
{
use ContainsComputedData, ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, Localizable, Publishable, Revisable, Searchable, TracksLastModified, TracksQueriedColumns, TracksQueriedRelations;
Expand Down Expand Up @@ -399,6 +402,12 @@ public function saveQuietly()

public function save()
{
if ($this->hasOriginCycle()) {
throw ValidationException::withMessages([
'origin' => __('Origin sites cannot reference each other in a loop.'),
]);
}

$isNew = is_null(Facades\Entry::find($this->id()));

$withEvents = $this->withEvents;
Expand Down Expand Up @@ -848,10 +857,21 @@ public function in($locale)
public function ancestors()
{
$ancestors = collect();
$seen = [];

$origin = $this->origin();

while ($origin) {
$id = $origin->id();

if ($id !== null && isset($seen[$id])) {
break;
}

if ($id !== null) {
$seen[$id] = true;
}

$ancestors->push($origin);
$origin = $origin->origin();
}
Expand Down
1 change: 0 additions & 1 deletion src/Facades/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* @method static mixed authorized()
* @method static mixed default()
* @method static bool hasMultiple()
* @method static \Illuminate\Support\Collection filterByGroup($handles, ?string $siteHandle)
* @method static mixed get($handle)
* @method static mixed findByUrl($url)
* @method static mixed current()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public function show(Request $request, $collection, $entry, $revision)
'exists' => $exists,
'root' => $exists ? $localized->isRoot() : false,
'origin' => $exists ? $localized->id() === optional($entry->origin())->id() : null,
'origin_handle' => $exists ? optional($localized->origin())->locale() : null,
'published' => $exists ? $localized->published() : false,
'url' => $exists ? $localized->editUrl() : null,
];
Expand Down
25 changes: 24 additions & 1 deletion src/Http/Controllers/CP/Globals/GlobalsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public function update(Request $request, $set)
->filter(fn ($site) => $site['enabled'])
->mapWithKeys(fn ($site) => [$site['handle'] => $site['origin']]);

$this->validateAcyclicOrigins($sites);

$set->sites($sites);
}

Expand All @@ -118,6 +120,27 @@ public function update(Request $request, $set)
return response('', 204);
}

private function validateAcyclicOrigins($origins)
{
$origins = collect($origins);

foreach ($origins as $start => $origin) {
$seen = [];
$current = $start;

while ($current) {
if (isset($seen[$current])) {
throw \Illuminate\Validation\ValidationException::withMessages([
'sites' => __('Origin sites cannot reference each other in a loop.'),
]);
}

$seen[$current] = true;
$current = $origins->get($current);
}
}
}

public function create()
{
$this->authorize('create', GlobalSetContract::class);
Expand Down Expand Up @@ -202,7 +225,7 @@ protected function editFormBlueprint($set)

if (Site::multiEnabled()) {
$fields['sites'] = [
'display' => __('Sites'),
'display' => __('Localizable'),
'fields' => [
'sites' => [
'type' => 'global_set_sites',
Expand Down
2 changes: 1 addition & 1 deletion src/Routing/Routable.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function slug($slug = null)
return null;
}

$lang = method_exists($this, 'site') ? $this->site()->lang() : null;
$lang = method_exists($this, 'site') ? $this->site()?->lang() : null;

return Str::slug($slug, '-', $lang);
})->args(func_get_args());
Expand Down
23 changes: 0 additions & 23 deletions src/Sites/Sites.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,6 @@ public function hasMultiple()
return $this->sites->count() > 1;
}

public function filterByGroup($handles, ?string $siteHandle)
{
if (! $siteHandle || ! ($site = $this->get($siteHandle))) {
return collect($handles);
}

$groupKey = $site->groupHandle() ?? $site->group();

if (! $groupKey) {
return collect($handles);
}

return collect($handles)->filter(function ($handle) use ($groupKey) {
$other = $this->get($handle);

if (! $other) {
return false;
}

return ($other->groupHandle() ?? $other->group()) === $groupKey;
});
}

public function get($handle)
{
return $this->sites->get($handle);
Expand Down
7 changes: 4 additions & 3 deletions src/Stache/Stache.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\Log;
use Statamic\Events\StacheCleared;
use Statamic\Events\StacheWarmed;
use Statamic\Extensions\FileStore;
Expand Down Expand Up @@ -271,7 +272,7 @@ protected function shouldUseParallelWarming($stores): bool
// Disable parallel processing if using Redis cache (serialization issues)
$cacheDriver = config('statamic.stache.cache_store', config('cache.default'));
if ($cacheDriver === 'redis') {
\Log::info('Parallel warming disabled due to Redis cache driver');
Log::info('Parallel warming disabled due to Redis cache driver');

return false;
}
Expand Down Expand Up @@ -303,12 +304,12 @@ protected function warmInParallel($stores)
$driver = $config['concurrency_driver'] ?? 'process';

if (empty($closures)) {
\Log::info('Closures are empty, skipping parallel warming');
Log::info('Closures are empty, skipping parallel warming');
}

Concurrency::driver($driver)->run($closures);
} catch (\Exception $e) {
\Log::warning('Parallel warming failed, falling back to sequential: '.$e->getMessage());
Log::warning('Parallel warming failed, falling back to sequential: '.$e->getMessage());
$stores->each->warm();
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Tags/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Tags;

use Illuminate\Support\Facades\Log;
use Statamic\Assets\Asset as AssetModel;
use Statamic\Assets\AssetCollection;
use Statamic\Contracts\Query\Builder;
Expand Down Expand Up @@ -88,7 +89,7 @@ public function index()
protected function assetsFromContainer($id, $path)
{
if (! $id && ! $path) {
\Log::debug('No asset container ID or path was specified.');
Log::debug('No asset container ID or path was specified.');

return collect();
}
Expand Down
Loading
Loading