From da7ace079fe8f3d2646ae23d5058309d2a679b07 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 22 May 2026 10:33:50 +0100 Subject: [PATCH] Fix entry revision localizations to filter unauthorized sites The revision preview endpoint was exposing all collection sites in the localizations array regardless of user permissions. This allowed users with restricted site access to view unauthorized site details in the API response. Apply the same authorization filtering used in EntriesController by adding getAuthorizedSitesForCollection() to filter sites based on the current user's view permissions. Fixes #14697 Co-Authored-By: Claude Opus 4.5 --- .../Collections/EntryRevisionsController.php | 9 +++- tests/Feature/Entries/EntryRevisionsTest.php | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Collections/EntryRevisionsController.php b/src/Http/Controllers/CP/Collections/EntryRevisionsController.php index 4d907b158bb..f0e7e359b14 100644 --- a/src/Http/Controllers/CP/Collections/EntryRevisionsController.php +++ b/src/Http/Controllers/CP/Collections/EntryRevisionsController.php @@ -83,7 +83,7 @@ public function show(Request $request, $collection, $entry, $revision) 'readOnly' => true, 'published' => $entry->published(), 'locale' => $entry->locale(), - 'localizations' => $entry->collection()->sites()->map(function ($handle) use ($entry) { + 'localizations' => $this->getAuthorizedSitesForCollection($entry->collection())->map(function ($handle) use ($entry) { $localized = $entry->in($handle); $exists = $localized !== null; @@ -120,4 +120,11 @@ protected function collectionToArray($collection) 'url' => cp_route('collections.show', $collection->handle()), ]; } + + private function getAuthorizedSitesForCollection($collection) + { + return $collection + ->sites() + ->filter(fn ($handle) => User::current()->can('view', Site::get($handle))); + } } diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index 5ffe0a5a334..5fe4dc34690 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -699,6 +699,57 @@ public function localized_entry_with_localizable_date_keeps_its_own_date_when_re ); } + #[Test] + public function revision_localizations_only_includes_authorized_sites() + { + $this->setSites([ + 'en' => ['url' => 'http://localhost/', 'locale' => 'en'], + 'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'], + 'de' => ['url' => 'http://localhost/de/', 'locale' => 'de'], + ]); + + $this->setTestBlueprint('test', ['foo' => ['type' => 'text']]); + $this->setTestRoles(['test' => [ + 'access cp', + 'view blog entries', + 'access en site', + 'access fr site', + // Note: no 'access de site' permission + ]]); + $user = User::make()->id('user-1')->assignRole('test')->save(); + + $this->collection->sites(['en', 'fr', 'de'])->save(); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('blog') + ->locale('en') + ->published(true) + ->date('2010-12-25') + ->data([ + 'blueprint' => 'test', + 'title' => 'Original title', + 'foo' => 'bar', + ])->create(); + + $revision = tap($entry->makeRevision(), function ($copy) { + $copy->message('Revision one'); + $copy->date(Carbon::parse('2017-02-01')); + }); + $revision->save(); + + $response = $this + ->actingAs($user) + ->getJson($entry->revisionsUrl().'/'.$revision->date()->timestamp) + ->assertOk(); + + $localizations = $response->json('localizations'); + + // User should only see en and fr sites, not de + $this->assertCount(2, $localizations); + $this->assertEquals(['en', 'fr'], array_column($localizations, 'handle')); + } + private function setTestBlueprint($handle, $fields) { $blueprint = Blueprint::makeFromFields($fields)->setHandle($handle);