From 71120eb894e0374dbf871f104cc979e9b505411e Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 12 Jun 2025 12:47:36 +0200 Subject: [PATCH 01/27] Add permission --- resources/lang/en/permissions.php | 1 + src/Auth/CorePermissions.php | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/lang/en/permissions.php b/resources/lang/en/permissions.php index e84f8db8121..79e040f1124 100644 --- a/resources/lang/en/permissions.php +++ b/resources/lang/en/permissions.php @@ -31,6 +31,7 @@ 'publish_{collection}_entries_desc' => 'Ability to change from draft to published and vice versa', 'reorder_{collection}_entries' => 'Reorder entries', 'reorder_{collection}_entries_desc' => 'Enables drag and drop reordering', + 'view_other_authors_{collection}_entries' => "View other authors' entries", 'edit_other_authors_{collection}_entries' => "Edit other authors' entries", 'publish_other_authors_{collection}_entries' => "Manage publish state of other authors' entries", 'delete_other_authors_{collection}_entries' => "Delete other authors' entries", diff --git a/src/Auth/CorePermissions.php b/src/Auth/CorePermissions.php index 1931c25b8d3..02c289c4fa4 100644 --- a/src/Auth/CorePermissions.php +++ b/src/Auth/CorePermissions.php @@ -97,9 +97,11 @@ protected function registerCollections() $this->permission('delete {collection} entries'), $this->permission('publish {collection} entries'), $this->permission('reorder {collection} entries'), - $this->permission('edit other authors {collection} entries')->children([ - $this->permission('publish other authors {collection} entries'), - $this->permission('delete other authors {collection} entries'), + $this->permission('view other authors {collection} entries')->children([ + $this->permission('edit other authors {collection} entries')->children([ + $this->permission('publish other authors {collection} entries'), + $this->permission('delete other authors {collection} entries'), + ]), ]), ]), ])->replacements('collection', function () { From 39950dbc8cffd768e2bdedd32422181dd7b6d4b6 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 12 Jun 2025 12:57:25 +0200 Subject: [PATCH 02/27] Restrict other authors from viewing entries --- src/Policies/EntryPolicy.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Policies/EntryPolicy.php b/src/Policies/EntryPolicy.php index ba6f7ab9e57..dff4f3a62d7 100644 --- a/src/Policies/EntryPolicy.php +++ b/src/Policies/EntryPolicy.php @@ -30,6 +30,10 @@ public function view($user, $entry) return false; } + if ($this->hasAnotherAuthor($user, $entry)) { + return $user->hasPermission("view other authors {$entry->collectionHandle()} entries"); + } + return $this->edit($user, $entry) || $user->hasPermission("view {$entry->collectionHandle()} entries"); } From 91b22dd4e4b1e7428abf442b8b51d2857d59eaeb Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 12 Jun 2025 15:23:55 +0200 Subject: [PATCH 03/27] =?UTF-8?q?Only=20show=20author=E2=80=99s=20entries?= =?UTF-8?q?=20in=20listings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also show entries with a blueprint that doesn’t have an author field --- .../CP/Collections/EntriesController.php | 13 +++++++++++++ src/Policies/EntryPolicy.php | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index bae59f2b432..5c14ed84f1a 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -84,6 +84,19 @@ protected function indexQuery($collection) $query->whereIn('site', Site::authorized()->map->handle()->all()); } + if (User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) { + // Mirror the behavior of the hasAnotherAuthor() method in the EntryPolicy. + $blueprintsWithoutAuthor = $collection->entryBlueprints() + ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + ->map->handle()->all(); + + $query->where(function ($query) use ($blueprintsWithoutAuthor) { + $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()); + }); + } + return $query; } diff --git a/src/Policies/EntryPolicy.php b/src/Policies/EntryPolicy.php index dff4f3a62d7..be2853e1994 100644 --- a/src/Policies/EntryPolicy.php +++ b/src/Policies/EntryPolicy.php @@ -53,6 +53,11 @@ public function edit($user, $entry) return $user->hasPermission("edit {$entry->collectionHandle()} entries"); } + public function viewOtherAuthorsEntries($user, $collection) + { + return $user->hasPermission("view other authors {$collection->handle()} entries"); + } + public function editOtherAuthorsEntries($user, $collection, $blueprint = null) { $blueprint = $blueprint ?? $collection->entryBlueprint(); From 0544e442384ae74eb93739800db6e36e34300195 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 12 Jun 2025 16:17:51 +0200 Subject: [PATCH 04/27] Fix entries count --- .../CP/Collections/CollectionsController.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Collections/CollectionsController.php b/src/Http/Controllers/CP/Collections/CollectionsController.php index 6765779a4db..a85eef9a9fb 100644 --- a/src/Http/Controllers/CP/Collections/CollectionsController.php +++ b/src/Http/Controllers/CP/Collections/CollectionsController.php @@ -55,10 +55,25 @@ private function collections() || User::current()->can('view', $collection) && $collection->sites()->contains(Site::selected()->handle()); })->map(function ($collection) { + $entriesCount = $collection->queryEntries() + ->where('site', Site::selected()) + ->when(User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection]), function ($query) use ($collection) { + $blueprintsWithoutAuthor = $collection->entryBlueprints() + ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + ->map->handle()->all(); + + $query->where(function ($query) use ($blueprintsWithoutAuthor) { + $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()); + }); + }) + ->count(); + return [ 'id' => $collection->handle(), 'title' => $collection->title(), - 'entries' => $collection->queryEntries()->where('site', Site::selected())->count(), + 'entries' => $entriesCount, 'edit_url' => $collection->editUrl(), 'delete_url' => $collection->deleteUrl(), 'entries_url' => cp_route('collections.show', $collection->handle()), From 9aa271948146b6fd149d749c7f0869e2e458ef60 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 09:09:05 +0200 Subject: [PATCH 05/27] Make Erin happy with shorter syntax --- .../Controllers/CP/Collections/CollectionsController.php | 9 ++++----- .../Controllers/CP/Collections/EntriesController.php | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/CollectionsController.php b/src/Http/Controllers/CP/Collections/CollectionsController.php index a85eef9a9fb..8b3aa925580 100644 --- a/src/Http/Controllers/CP/Collections/CollectionsController.php +++ b/src/Http/Controllers/CP/Collections/CollectionsController.php @@ -62,11 +62,10 @@ private function collections() ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) ->map->handle()->all(); - $query->where(function ($query) use ($blueprintsWithoutAuthor) { - $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()); - }); + $query->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()) + ); }) ->count(); diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 5c14ed84f1a..eaa60d2485c 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -90,11 +90,10 @@ protected function indexQuery($collection) ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) ->map->handle()->all(); - $query->where(function ($query) use ($blueprintsWithoutAuthor) { - $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()); - }); + $query->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()) + ); } return $query; From ba02dceecbf34ed961d37b2a902e47d600d96bd6 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 09:32:20 +0200 Subject: [PATCH 06/27] Handle entries fieldtype --- src/Fieldtypes/Entries.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 6816ccd5f1e..2506f556076 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -144,6 +144,20 @@ public function getIndexItems($request) $query->whereIn('blueprint', $blueprints); } + collect($this->getConfiguredCollections()) + ->map(fn ($handle) => Collection::findByHandle($handle)) + ->filter(fn ($collection) => User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) + ->each(function ($collection) use ($query) { + $blueprintsWithoutAuthor = $collection->entryBlueprints() + ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + ->map->handle()->all(); + + $query->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()) + ); + }); + $this->activeFilterBadges = $this->queryFilters($query, $filters, $this->getSelectionFilterContext()); if ($sort = $this->getSortColumn($request)) { From 9fd7f56c3c0d9bf31b5dcff8383bd1ab0d710190 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 10:02:50 +0200 Subject: [PATCH 07/27] Handle tree view --- src/Structures/CollectionStructure.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Structures/CollectionStructure.php b/src/Structures/CollectionStructure.php index 3edb98f43a6..58d8d159da4 100644 --- a/src/Structures/CollectionStructure.php +++ b/src/Structures/CollectionStructure.php @@ -2,10 +2,12 @@ namespace Statamic\Structures; -use Statamic\Contracts\Structures\CollectionTree; -use Statamic\Contracts\Structures\CollectionTreeRepository; +use Statamic\Facades\User; use Statamic\Facades\Blink; use Statamic\Facades\Collection; +use Statamic\Contracts\Entries\Entry; +use Statamic\Contracts\Structures\CollectionTree; +use Statamic\Contracts\Structures\CollectionTreeRepository; class CollectionStructure extends Structure { @@ -74,6 +76,16 @@ public function validateTree(array $tree, string $locale): array $thisCollectionsEntries = $this->collection()->queryEntries() ->where('site', $locale) + ->when(User::current()->cant('view-other-authors-entries', [Entry::class, $this->collection()]), function ($query) { + $blueprintsWithoutAuthor = $this->collection()->entryBlueprints() + ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + ->map->handle()->all(); + + $query->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()) + ); + }) ->pluck('id'); $otherCollectionEntries = $entryIds->diff($thisCollectionsEntries); From 3fade8b1774c503420fc65846f50432cfe63eac7 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 10:09:53 +0200 Subject: [PATCH 08/27] =?UTF-8?q?=F0=9F=8D=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Fieldtypes/Entries.php | 8 ++++---- src/Structures/CollectionStructure.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 2506f556076..0f934690828 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -152,10 +152,10 @@ public function getIndexItems($request) ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) ->map->handle()->all(); - $query->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()) - ); + $query->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()) + ); }); $this->activeFilterBadges = $this->queryFilters($query, $filters, $this->getSelectionFilterContext()); diff --git a/src/Structures/CollectionStructure.php b/src/Structures/CollectionStructure.php index 58d8d159da4..a12ca424d8e 100644 --- a/src/Structures/CollectionStructure.php +++ b/src/Structures/CollectionStructure.php @@ -2,12 +2,12 @@ namespace Statamic\Structures; -use Statamic\Facades\User; -use Statamic\Facades\Blink; -use Statamic\Facades\Collection; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Structures\CollectionTree; use Statamic\Contracts\Structures\CollectionTreeRepository; +use Statamic\Facades\Blink; +use Statamic\Facades\Collection; +use Statamic\Facades\User; class CollectionStructure extends Structure { From 6f7b7901792292efd9cd7095323ba2d5d3d1aed1 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 10:55:19 +0200 Subject: [PATCH 09/27] Move filtering to the controller --- .../CP/Collections/CollectionTreeController.php | 13 +++++++++++++ src/Structures/CollectionStructure.php | 12 ------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/CollectionTreeController.php b/src/Http/Controllers/CP/Collections/CollectionTreeController.php index fb8acc556cc..dbceedf5b7a 100644 --- a/src/Http/Controllers/CP/Collections/CollectionTreeController.php +++ b/src/Http/Controllers/CP/Collections/CollectionTreeController.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Contracts\Entries\Collection; +use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Facades\Entry; use Statamic\Facades\Site; use Statamic\Facades\User; @@ -24,6 +25,18 @@ public function index(Request $request, Collection $collection) 'site' => $site, ]); + if (User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) { + $entriesFromOtherAuthors = collect($pages) + ->map(fn ($page) => Entry::find($page['entry'])) + ->filter(fn ($entry) => $entry->blueprint()->hasField('author')) + ->filter(fn ($entry) => ! $entry->authors()->contains(User::current()->id())) + ->map->id(); + + $pages = collect($pages) + ->filter(fn ($page) => ! $entriesFromOtherAuthors->contains($page['entry'])) + ->values()->all(); + } + return ['pages' => $pages]; } diff --git a/src/Structures/CollectionStructure.php b/src/Structures/CollectionStructure.php index a12ca424d8e..3edb98f43a6 100644 --- a/src/Structures/CollectionStructure.php +++ b/src/Structures/CollectionStructure.php @@ -2,12 +2,10 @@ namespace Statamic\Structures; -use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Structures\CollectionTree; use Statamic\Contracts\Structures\CollectionTreeRepository; use Statamic\Facades\Blink; use Statamic\Facades\Collection; -use Statamic\Facades\User; class CollectionStructure extends Structure { @@ -76,16 +74,6 @@ public function validateTree(array $tree, string $locale): array $thisCollectionsEntries = $this->collection()->queryEntries() ->where('site', $locale) - ->when(User::current()->cant('view-other-authors-entries', [Entry::class, $this->collection()]), function ($query) { - $blueprintsWithoutAuthor = $this->collection()->entryBlueprints() - ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) - ->map->handle()->all(); - - $query->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()) - ); - }) ->pluck('id'); $otherCollectionEntries = $entryIds->diff($thisCollectionsEntries); From 24d58925053c6331f9cea6027c5c5a9acd895158 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 16:15:27 +0200 Subject: [PATCH 10/27] Fix issue with multiple collections --- src/Fieldtypes/Entries.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 0f934690828..e5b5d810095 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -152,10 +152,12 @@ public function getIndexItems($request) ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) ->map->handle()->all(); - $query->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()) - ); + $query + ->whereNotIn('collection', [$collection->handle()]) + ->orWhere(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithoutAuthor) + ->orWhere('author', User::current()->id()) + ); }); $this->activeFilterBadges = $this->queryFilters($query, $filters, $this->getSelectionFilterContext()); From 3006d6fa2637256a96946549e3d1e987eacaeeaa Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 17:06:56 +0200 Subject: [PATCH 11/27] Dynamically change author field visibility --- .../CP/Collections/EntriesController.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index eaa60d2485c..22b5714766e 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -113,9 +113,7 @@ public function edit(Request $request, $collection, $entry) $blueprint->setParent($entry); - if (User::current()->cant('edit-other-authors-entries', [EntryContract::class, $collection, $blueprint])) { - $blueprint->ensureFieldHasConfig('author', ['visibility' => 'read_only']); - } + $this->modifyAuthorFieldVisibility($collection, $blueprint); [$values, $meta, $extraValues] = $this->extractFromFields($entry, $blueprint); @@ -312,9 +310,7 @@ public function create(Request $request, $collection, $site) throw new \Exception(__('A valid blueprint is required.')); } - if (User::current()->cant('edit-other-authors-entries', [EntryContract::class, $collection, $blueprint])) { - $blueprint->ensureFieldHasConfig('author', ['visibility' => 'read_only']); - } + $this->modifyAuthorFieldVisibility($collection, $blueprint); $values = Entry::make()->collection($collection)->values()->all(); @@ -580,4 +576,15 @@ protected function ensureCollectionIsAvailableOnSite($collection, $site) return redirect()->back()->with('error', __('Collection is not available on site ":handle".', ['handle' => $site->handle])); } } + + protected function modifyAuthorFieldVisibility($collection, $blueprint) + { + $authorVisibility = match (true) { + User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection]) => 'hidden', + User::current()->cant('edit-other-authors-entries', [EntryContract::class, $collection, $blueprint]) => 'read_only', + default => 'visible', + }; + + $blueprint->ensureFieldHasConfig('author', ['visibility' => $authorVisibility]); + } } From e114128844bf2512f0318a3569e938f08bc015c2 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 13 Jun 2025 17:17:55 +0200 Subject: [PATCH 12/27] Also remove author column --- src/Http/Resources/CP/Entries/Entries.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Http/Resources/CP/Entries/Entries.php b/src/Http/Resources/CP/Entries/Entries.php index b9172f8676c..8172f095eac 100644 --- a/src/Http/Resources/CP/Entries/Entries.php +++ b/src/Http/Resources/CP/Entries/Entries.php @@ -3,7 +3,9 @@ namespace Statamic\Http\Resources\CP\Entries; use Illuminate\Http\Resources\Json\ResourceCollection; +use Statamic\Contracts\Entries\Entry; use Statamic\CP\Column; +use Statamic\Facades\User; use Statamic\Http\Resources\CP\Concerns\HasRequestedColumns; class Entries extends ResourceCollection @@ -42,6 +44,10 @@ private function setColumns() $columns->put('status', $status); + if (User::current()->cant('view-other-authors-entries', [Entry::class, $this->blueprint->parent()])) { + $columns->get('author')->listable(false); + } + if ($key = $this->columnPreferenceKey) { $columns->setPreferred($key); } From 8b3c4f2adba03075393a4d3863bd89aeb00da0a5 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Wed, 25 Jun 2025 16:00:32 +0200 Subject: [PATCH 13/27] Fix tests --- src/Http/Resources/CP/Entries/Entries.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Resources/CP/Entries/Entries.php b/src/Http/Resources/CP/Entries/Entries.php index 8172f095eac..1fc5437f0c9 100644 --- a/src/Http/Resources/CP/Entries/Entries.php +++ b/src/Http/Resources/CP/Entries/Entries.php @@ -45,7 +45,7 @@ private function setColumns() $columns->put('status', $status); if (User::current()->cant('view-other-authors-entries', [Entry::class, $this->blueprint->parent()])) { - $columns->get('author')->listable(false); + $columns->get('author')?->listable(false); } if ($key = $this->columnPreferenceKey) { From 38090d508d50825710ec4329f0f40977dbe1a86c Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Wed, 25 Jun 2025 16:13:51 +0200 Subject: [PATCH 14/27] Add update script --- .../AddViewOtherAuthorsEntriesPermissions.php | 43 ++++++++++ ...ViewOtherAuthorsEntriesPermissionsTest.php | 82 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/UpdateScripts/AddViewOtherAuthorsEntriesPermissions.php create mode 100644 tests/UpdateScripts/AddViewOtherAuthorsEntriesPermissionsTest.php diff --git a/src/UpdateScripts/AddViewOtherAuthorsEntriesPermissions.php b/src/UpdateScripts/AddViewOtherAuthorsEntriesPermissions.php new file mode 100644 index 00000000000..3262189f08d --- /dev/null +++ b/src/UpdateScripts/AddViewOtherAuthorsEntriesPermissions.php @@ -0,0 +1,43 @@ +isUpdatingTo('5.59'); + } + + public function update() + { + Role::all()->each(fn ($role) => $this->updateRole($role)); + } + + private function updateRole($role) + { + $this->getMatchingPermissions($role->permissions(), '/^edit other authors (\w+) entries$/') + ->filter + ->capture + ->each(function ($match) use ($role) { + $role->addPermission("view other authors {$match->capture} entries"); + }); + + $role->save(); + } + + private function getMatchingPermissions($permissions, $regex) + { + return $permissions + ->map(function ($permission) use ($regex) { + $found = preg_match($regex, $permission, $matches); + + return $found + ? (object) ['permission' => $permission, 'capture' => $matches[1] ?? null] + : null; + }) + ->filter(); + } +} diff --git a/tests/UpdateScripts/AddViewOtherAuthorsEntriesPermissionsTest.php b/tests/UpdateScripts/AddViewOtherAuthorsEntriesPermissionsTest.php new file mode 100644 index 00000000000..cffdb5d965c --- /dev/null +++ b/tests/UpdateScripts/AddViewOtherAuthorsEntriesPermissionsTest.php @@ -0,0 +1,82 @@ +title('Webmaster') + ->handle('webmaster') + ->permissions('super') + ->save(); + + Role::make() + ->title('Author') + ->handle('author') + ->permissions([ + 'access cp', + 'create blog entries', + 'edit blog entries', + 'create news entries', + 'edit news entries', + ]) + ->save(); + + Role::make() + ->title('Blog Admin') + ->handle('blog_admin') + ->permissions([ + 'access cp', + 'create blog entries', + 'edit blog entries', + 'publish blog entries', + 'delete blog entries', + 'reorder blog entries', + 'edit other authors blog entries', + 'publish other authors blog entries', + 'delete other authors blog entries', + ]) + ->save(); + + $this->runUpdateScript(AddViewOtherAuthorsEntriesPermissions::class); + + $expectedAuthor = [ + 'access cp', + 'create blog entries', + 'edit blog entries', + 'create news entries', + 'edit news entries', + ]; + + $expectedBlogAdmin = [ + 'access cp', + 'create blog entries', + 'edit blog entries', + 'publish blog entries', + 'delete blog entries', + 'reorder blog entries', + 'edit other authors blog entries', + 'publish other authors blog entries', + 'delete other authors blog entries', + 'view other authors blog entries', // New permission + ]; + + $this->assertTrue(Role::find('webmaster')->isSuper()); + $this->assertEquals($expectedAuthor, Role::find('author')->permissions()->all()); + $this->assertEquals($expectedBlogAdmin, Role::find('blog_admin')->permissions()->all()); + + Role::all()->each->delete(); // Clean up + } +} From f718e21c0a179027013849014795f51589524507 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 27 Jun 2025 11:48:30 +0200 Subject: [PATCH 15/27] Add support for multiple authors --- src/Fieldtypes/Entries.php | 18 ++++++++++++++---- .../CP/Collections/CollectionsController.php | 18 ++++++++++++++---- .../CP/Collections/EntriesController.php | 19 ++++++++++++++----- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index e5b5d810095..264cf1e03d3 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -148,15 +148,25 @@ public function getIndexItems($request) ->map(fn ($handle) => Collection::findByHandle($handle)) ->filter(fn ($collection) => User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) ->each(function ($collection) use ($query) { - $blueprintsWithoutAuthor = $collection->entryBlueprints() - ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + $blueprints = $collection->entryBlueprints(); + + $blueprintsWithAuthor = $blueprints + ->filter(fn ($blueprint) => $blueprint->hasField('author')) + ->map->handle()->all(); + + $blueprintsWithoutAuthor = $blueprints + ->diff($blueprintsWithAuthor) ->map->handle()->all(); $query ->whereNotIn('collection', [$collection->handle()]) ->orWhere(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()) + ->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithAuthor) + ->whereIn('author', [User::current()->id()]) + ->orWhereJsonContains('author', User::current()->id()) + ) + ->orWhereIn('blueprint', $blueprintsWithoutAuthor) ); }); diff --git a/src/Http/Controllers/CP/Collections/CollectionsController.php b/src/Http/Controllers/CP/Collections/CollectionsController.php index 8b3aa925580..af517f9bfd3 100644 --- a/src/Http/Controllers/CP/Collections/CollectionsController.php +++ b/src/Http/Controllers/CP/Collections/CollectionsController.php @@ -58,13 +58,23 @@ private function collections() $entriesCount = $collection->queryEntries() ->where('site', Site::selected()) ->when(User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection]), function ($query) use ($collection) { - $blueprintsWithoutAuthor = $collection->entryBlueprints() - ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + $blueprints = $collection->entryBlueprints(); + + $blueprintsWithAuthor = $blueprints + ->filter(fn ($blueprint) => $blueprint->hasField('author')) + ->map->handle()->all(); + + $blueprintsWithoutAuthor = $blueprints + ->diff($blueprintsWithAuthor) ->map->handle()->all(); $query->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()) + ->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithAuthor) + ->whereIn('author', [User::current()->id()]) + ->orWhereJsonContains('author', User::current()->id()) + ) + ->orWhereIn('blueprint', $blueprintsWithoutAuthor) ); }) ->count(); diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 22b5714766e..802f0f29007 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -85,14 +85,23 @@ protected function indexQuery($collection) } if (User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) { - // Mirror the behavior of the hasAnotherAuthor() method in the EntryPolicy. - $blueprintsWithoutAuthor = $collection->entryBlueprints() - ->filter(fn ($blueprint) => ! $blueprint->hasField('author')) + $blueprints = $collection->entryBlueprints(); + + $blueprintsWithAuthor = $blueprints + ->filter(fn ($blueprint) => $blueprint->hasField('author')) + ->map->handle()->all(); + + $blueprintsWithoutAuthor = $blueprints + ->diff($blueprintsWithAuthor) ->map->handle()->all(); $query->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithoutAuthor) - ->orWhere('author', User::current()->id()) + ->where(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithAuthor) + ->whereIn('author', [User::current()->id()]) + ->orWhereJsonContains('author', User::current()->id()) + ) + ->orWhereIn('blueprint', $blueprintsWithoutAuthor) ); } From fe47c01e28adfd65f539667b74004fc4cc834d8c Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 27 Jun 2025 12:55:09 +0200 Subject: [PATCH 16/27] Abstract --- src/Fieldtypes/Entries.php | 27 ++----------- .../CP/Collections/CollectionsController.php | 26 +++--------- .../CP/Collections/EntriesController.php | 20 +--------- .../CP/Collections/QueriesAuthorEntries.php | 40 +++++++++++++++++++ 4 files changed, 52 insertions(+), 61 deletions(-) create mode 100644 src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index 264cf1e03d3..10c75864dda 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -16,6 +16,7 @@ use Statamic\Facades\Search; use Statamic\Facades\Site; use Statamic\Facades\User; +use Statamic\Http\Controllers\CP\Collections\QueriesAuthorEntries; use Statamic\Http\Resources\CP\Entries\EntriesFieldtypeEntries; use Statamic\Http\Resources\CP\Entries\EntriesFieldtypeEntry as EntryResource; use Statamic\Query\OrderedQueryBuilder; @@ -31,7 +32,8 @@ class Entries extends Relationship { - use QueriesFilters; + use QueriesAuthorEntries, + QueriesFilters; protected $categories = ['relationship']; protected $keywords = ['entry']; @@ -147,28 +149,7 @@ public function getIndexItems($request) collect($this->getConfiguredCollections()) ->map(fn ($handle) => Collection::findByHandle($handle)) ->filter(fn ($collection) => User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) - ->each(function ($collection) use ($query) { - $blueprints = $collection->entryBlueprints(); - - $blueprintsWithAuthor = $blueprints - ->filter(fn ($blueprint) => $blueprint->hasField('author')) - ->map->handle()->all(); - - $blueprintsWithoutAuthor = $blueprints - ->diff($blueprintsWithAuthor) - ->map->handle()->all(); - - $query - ->whereNotIn('collection', [$collection->handle()]) - ->orWhere(fn ($query) => $query - ->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithAuthor) - ->whereIn('author', [User::current()->id()]) - ->orWhereJsonContains('author', User::current()->id()) - ) - ->orWhereIn('blueprint', $blueprintsWithoutAuthor) - ); - }); + ->each(fn ($collection) => $this->queryAuthorEntries($query, $collection)); $this->activeFilterBadges = $this->queryFilters($query, $filters, $this->getSelectionFilterContext()); diff --git a/src/Http/Controllers/CP/Collections/CollectionsController.php b/src/Http/Controllers/CP/Collections/CollectionsController.php index af517f9bfd3..04075ef2250 100644 --- a/src/Http/Controllers/CP/Collections/CollectionsController.php +++ b/src/Http/Controllers/CP/Collections/CollectionsController.php @@ -24,6 +24,8 @@ class CollectionsController extends CpController { + use QueriesAuthorEntries; + public function index(Request $request) { $this->authorize('index', CollectionContract::class, __('You are not authorized to view collections.')); @@ -57,26 +59,10 @@ private function collections() })->map(function ($collection) { $entriesCount = $collection->queryEntries() ->where('site', Site::selected()) - ->when(User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection]), function ($query) use ($collection) { - $blueprints = $collection->entryBlueprints(); - - $blueprintsWithAuthor = $blueprints - ->filter(fn ($blueprint) => $blueprint->hasField('author')) - ->map->handle()->all(); - - $blueprintsWithoutAuthor = $blueprints - ->diff($blueprintsWithAuthor) - ->map->handle()->all(); - - $query->where(fn ($query) => $query - ->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithAuthor) - ->whereIn('author', [User::current()->id()]) - ->orWhereJsonContains('author', User::current()->id()) - ) - ->orWhereIn('blueprint', $blueprintsWithoutAuthor) - ); - }) + ->when( + User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection]), + fn ($query) => $this->queryAuthorEntries($query, $collection) + ) ->count(); return [ diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 802f0f29007..19ba3486a2a 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -25,6 +25,7 @@ class EntriesController extends CpController { use ExtractsFromEntryFields, + QueriesAuthorEntries, QueriesFilters; public function index(FilteredRequest $request, $collection) @@ -85,24 +86,7 @@ protected function indexQuery($collection) } if (User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) { - $blueprints = $collection->entryBlueprints(); - - $blueprintsWithAuthor = $blueprints - ->filter(fn ($blueprint) => $blueprint->hasField('author')) - ->map->handle()->all(); - - $blueprintsWithoutAuthor = $blueprints - ->diff($blueprintsWithAuthor) - ->map->handle()->all(); - - $query->where(fn ($query) => $query - ->where(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithAuthor) - ->whereIn('author', [User::current()->id()]) - ->orWhereJsonContains('author', User::current()->id()) - ) - ->orWhereIn('blueprint', $blueprintsWithoutAuthor) - ); + $this->queryAuthorEntries($query, $collection); } return $query; diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php new file mode 100644 index 00000000000..97b8447e722 --- /dev/null +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -0,0 +1,40 @@ +where(fn ($query) => $query + ->whereNotIn('collection', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections + ->orWhere(fn ($query) => $query + ->whereIn('blueprint', $this->blueprintsWithAuthor($collection->entryBlueprints())) + ->whereIn('author', [User::current()->id()]) + ->orWhereJsonContains('author', User::current()->id()) + ) + ->orWhereIn('blueprint', $this->blueprintsWithoutAuthor($collection->entryBlueprints())) + ); + } + + protected function blueprintsWithAuthor(FileCollection $blueprints): array + { + return $blueprints + ->filter(fn (Blueprint $blueprint) => $blueprint->hasField('author')) + ->map->handle()->all(); + } + + protected function blueprintsWithoutAuthor(FileCollection $blueprints): array + { + return $blueprints + ->filter(fn (Blueprint $blueprint) => ! $blueprint->hasField('author')) + ->map->handle()->all(); + } +} From 2cddf2ab5e45bc9dc1addefd82479f8e98a82a99 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 27 Jun 2025 13:11:42 +0200 Subject: [PATCH 17/27] Fix tests --- .../Controllers/CP/Collections/QueriesAuthorEntries.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php index 97b8447e722..587e7bccef3 100644 --- a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -2,11 +2,11 @@ namespace Statamic\Http\Controllers\CP\Collections; +use Illuminate\Support\Collection as SupportCollection; use Statamic\Contracts\Entries\Collection; use Statamic\Contracts\Query\Builder; use Statamic\Facades\User; use Statamic\Fields\Blueprint; -use Statamic\Support\FileCollection; trait QueriesAuthorEntries { @@ -24,14 +24,14 @@ protected function queryAuthorEntries(Builder $query, Collection $collection): v ); } - protected function blueprintsWithAuthor(FileCollection $blueprints): array + protected function blueprintsWithAuthor(SupportCollection $blueprints): array { return $blueprints ->filter(fn (Blueprint $blueprint) => $blueprint->hasField('author')) ->map->handle()->all(); } - protected function blueprintsWithoutAuthor(FileCollection $blueprints): array + protected function blueprintsWithoutAuthor(SupportCollection $blueprints): array { return $blueprints ->filter(fn (Blueprint $blueprint) => ! $blueprint->hasField('author')) From 475dbdd426f6cc23b26696494c65ac404a4370b2 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Fri, 27 Jun 2025 15:20:44 +0200 Subject: [PATCH 18/27] Add tests --- .../Feature/Entries/ViewEntryListingTest.php | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/Feature/Entries/ViewEntryListingTest.php b/tests/Feature/Entries/ViewEntryListingTest.php index 0d36b634ddc..372224b4128 100644 --- a/tests/Feature/Entries/ViewEntryListingTest.php +++ b/tests/Feature/Entries/ViewEntryListingTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Entries\Collection; use Statamic\Facades\User; +use Statamic\Fields\Blueprint; use Tests\FakesRoles; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -90,4 +91,84 @@ public function it_shows_only_entries_in_index_for_sites_user_can_access() $this->assertEquals($expected, $entries->pluck('slug')->all()); } + + #[Test] + public function it_shows_only_entries_in_index_the_user_can_access() + { + $this->setTestRole('view-own-entries', [ + 'access cp', + 'view test entries', + ]); + + $this->setTestRole('view-other-authors-entries', [ + 'access cp', + 'view test entries', + 'view other authors test entries', + ]); + + $userOne = tap(User::make()->assignRole('view-own-entries'))->save(); + $userTwo = tap(User::make()->assignRole('view-other-authors-entries'))->save(); + + Blueprint::make('with-author') + ->setNamespace('collections/test') + ->ensureField('author', []) + ->save(); + + Blueprint::make('without-author') + ->setNamespace('collections/test') + ->save(); + + $collection = tap(Collection::make('test'))->save(); + + EntryFactory::collection($collection) + ->slug('entry-user-one') + ->data(['blueprint' => 'with-author', 'author' => $userOne->id()]) + ->create(); + + EntryFactory::collection($collection) + ->slug('entry-user-two') + ->data(['blueprint' => 'with-author', 'author' => $userTwo->id()]) + ->create(); + + EntryFactory::collection($collection) + ->slug('entry-with-multiple-authors') + ->data(['blueprint' => 'with-author', 'author' => [$userOne->id(), $userTwo->id()]]) + ->create(); + + EntryFactory::collection($collection) + ->slug('entry-without-author') + ->data(['blueprint' => 'without-author']) + ->create(); + + $responseUserOne = $this + ->actingAs($userOne) + ->get(cp_route('collections.entries.index', ['collection' => 'test'])) + ->assertOk(); + + $entries = collect($responseUserOne->getData()->data); + + $expected = [ + 'entry-user-one', + 'entry-with-multiple-authors', + 'entry-without-author', + ]; + + $this->assertEquals($expected, $entries->pluck('slug')->all()); + + $responseUserTwo = $this + ->actingAs($userTwo) + ->get(cp_route('collections.entries.index', ['collection' => 'test'])) + ->assertOk(); + + $entries = collect($responseUserTwo->getData()->data); + + $expected = [ + 'entry-user-one', + 'entry-user-two', + 'entry-with-multiple-authors', + 'entry-without-author', + ]; + + $this->assertEquals($expected, $entries->pluck('slug')->all()); + } } From 6e538182fa3ba9a51c1c512c74f7abd3332cf648 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Tue, 5 Aug 2025 12:03:39 +0200 Subject: [PATCH 19/27] Allow users to override field visibility --- src/Entries/ChangeAuthorFieldVisibility.php | 28 +++++++++++++++++++ .../CP/Collections/EntriesController.php | 15 ---------- 2 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 src/Entries/ChangeAuthorFieldVisibility.php diff --git a/src/Entries/ChangeAuthorFieldVisibility.php b/src/Entries/ChangeAuthorFieldVisibility.php new file mode 100644 index 00000000000..7384876b466 --- /dev/null +++ b/src/Entries/ChangeAuthorFieldVisibility.php @@ -0,0 +1,28 @@ +entry) { + return; + } + + if (! $event->authenticatedUser) { + return; + } + + $authorVisibility = match (true) { + $event->authenticatedUser->cant('view-other-authors-entries', [Entry::class, $event->entry->collection()]) => 'hidden', + $event->authenticatedUser->cant('edit-other-authors-entries', [Entry::class, $event->entry->collection(), $event->blueprint]) => 'read_only', + default => 'visible', + }; + + $event->blueprint->ensureFieldHasConfig('author', ['visibility' => $authorVisibility]); + } +} diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 19ba3486a2a..2b55070d097 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -106,8 +106,6 @@ public function edit(Request $request, $collection, $entry) $blueprint->setParent($entry); - $this->modifyAuthorFieldVisibility($collection, $blueprint); - [$values, $meta, $extraValues] = $this->extractFromFields($entry, $blueprint); if ($hasOrigin = $entry->hasOrigin()) { @@ -303,8 +301,6 @@ public function create(Request $request, $collection, $site) throw new \Exception(__('A valid blueprint is required.')); } - $this->modifyAuthorFieldVisibility($collection, $blueprint); - $values = Entry::make()->collection($collection)->values()->all(); if ($collection->hasStructure() && $request->parent) { @@ -569,15 +565,4 @@ protected function ensureCollectionIsAvailableOnSite($collection, $site) return redirect()->back()->with('error', __('Collection is not available on site ":handle".', ['handle' => $site->handle])); } } - - protected function modifyAuthorFieldVisibility($collection, $blueprint) - { - $authorVisibility = match (true) { - User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection]) => 'hidden', - User::current()->cant('edit-other-authors-entries', [EntryContract::class, $collection, $blueprint]) => 'read_only', - default => 'visible', - }; - - $blueprint->ensureFieldHasConfig('author', ['visibility' => $authorVisibility]); - } } From a5f0ceafb9bcef2bd4d1513e2733be23dae29294 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Tue, 5 Aug 2025 12:10:10 +0200 Subject: [PATCH 20/27] Register event listener --- src/Providers/EventServiceProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Providers/EventServiceProvider.php b/src/Providers/EventServiceProvider.php index 0f8a9d2406c..48e3db1cef1 100755 --- a/src/Providers/EventServiceProvider.php +++ b/src/Providers/EventServiceProvider.php @@ -22,6 +22,7 @@ class EventServiceProvider extends ServiceProvider ], \Statamic\Events\EntryBlueprintFound::class => [ \Statamic\Entries\AddSiteColumnToBlueprint::class, + \Statamic\Entries\ChangeAuthorFieldVisibility::class, ], \Statamic\Events\ResponseCreated::class => [ \Statamic\Listeners\ClearState::class, From 7d9bc305bd324465436b5629ee42cebc4ba9be97 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Wed, 20 Aug 2025 13:11:24 +0200 Subject: [PATCH 21/27] Make sure permissions apply to search This also fixes an issue where the search would include unauthorized sites if the collection had a search index. --- .../CP/Collections/EntriesController.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 2b55070d097..3cdfecaa402 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -70,17 +70,17 @@ protected function indexQuery($collection) $query = $collection->queryEntries(); if ($search = request('search')) { - if ($collection->hasSearchIndex()) { - return $collection - ->searchIndex() - ->ensureExists() - ->search($search) - ->where('collection', $collection->handle()); - } - $query->where('title', 'like', '%'.$search.'%'); } + if ($search && $collection->hasSearchIndex()) { + $query = $collection + ->searchIndex() + ->ensureExists() + ->search($search) + ->where('collection', $collection->handle()); + } + if (Site::multiEnabled()) { $query->whereIn('site', Site::authorized()->map->handle()->all()); } From 50f32333b4a567383e6c6c259135e6c932b63056 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 12 Jan 2026 12:08:40 +0100 Subject: [PATCH 22/27] Compare against string --- src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php index 587e7bccef3..7178d1ae2b6 100644 --- a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -14,7 +14,7 @@ protected function queryAuthorEntries(Builder $query, Collection $collection): v { $query ->where(fn ($query) => $query - ->whereNotIn('collection', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections + ->whereNotIn('collectionHandle', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections ->orWhere(fn ($query) => $query ->whereIn('blueprint', $this->blueprintsWithAuthor($collection->entryBlueprints())) ->whereIn('author', [User::current()->id()]) From 75d76d02e0435f52235d411840ac74c173a19b07 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 12 Jan 2026 12:10:21 +0100 Subject: [PATCH 23/27] Use new whereHas --- src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php index 7178d1ae2b6..ccebd713210 100644 --- a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -17,8 +17,7 @@ protected function queryAuthorEntries(Builder $query, Collection $collection): v ->whereNotIn('collectionHandle', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections ->orWhere(fn ($query) => $query ->whereIn('blueprint', $this->blueprintsWithAuthor($collection->entryBlueprints())) - ->whereIn('author', [User::current()->id()]) - ->orWhereJsonContains('author', User::current()->id()) + ->whereHas('author', fn ($subquery) => $subquery->where('id', User::current()->id())) ) ->orWhereIn('blueprint', $this->blueprintsWithoutAuthor($collection->entryBlueprints())) ); From 6e732e237b18f7dde747950b059ff779b54036fd Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 12 Jan 2026 13:13:01 +0100 Subject: [PATCH 24/27] Fix issue if there are no blueprints with author --- .../CP/Collections/QueriesAuthorEntries.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php index ccebd713210..b600aca7a5e 100644 --- a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -12,15 +12,19 @@ trait QueriesAuthorEntries { protected function queryAuthorEntries(Builder $query, Collection $collection): void { - $query - ->where(fn ($query) => $query - ->whereNotIn('collectionHandle', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections + $blueprintsWithAuthor = $this->blueprintsWithAuthor($collection->entryBlueprints()); + $blueprintsWithoutAuthor = $this->blueprintsWithoutAuthor($collection->entryBlueprints()); + + $query->where(fn ($query) => $query + ->whereNotIn('collectionHandle', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections + ->when($blueprintsWithAuthor, fn ($query) => $query ->orWhere(fn ($query) => $query - ->whereIn('blueprint', $this->blueprintsWithAuthor($collection->entryBlueprints())) + ->whereIn('blueprint', $blueprintsWithAuthor) ->whereHas('author', fn ($subquery) => $subquery->where('id', User::current()->id())) ) - ->orWhereIn('blueprint', $this->blueprintsWithoutAuthor($collection->entryBlueprints())) - ); + ) + ->orWhereIn('blueprint', $blueprintsWithoutAuthor) + ); } protected function blueprintsWithAuthor(SupportCollection $blueprints): array From 1439cdcaf60b783ae2f55d2fefb94a16f3b3d8e9 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 12 Jan 2026 15:48:53 +0100 Subject: [PATCH 25/27] Fix test --- tests/Feature/Entries/ViewEntryListingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Entries/ViewEntryListingTest.php b/tests/Feature/Entries/ViewEntryListingTest.php index 372224b4128..1cbcb0d10c6 100644 --- a/tests/Feature/Entries/ViewEntryListingTest.php +++ b/tests/Feature/Entries/ViewEntryListingTest.php @@ -111,7 +111,7 @@ public function it_shows_only_entries_in_index_the_user_can_access() Blueprint::make('with-author') ->setNamespace('collections/test') - ->ensureField('author', []) + ->ensureField('author', ['type' => 'users']) ->save(); Blueprint::make('without-author') From 3674ed56e49965149489a5e7b6ef9a3ef2a14361 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 12 Jan 2026 16:02:37 +0100 Subject: [PATCH 26/27] No need to add queries if there is no author field --- .../CP/Collections/QueriesAuthorEntries.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php index b600aca7a5e..b05bbccf07a 100644 --- a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -15,13 +15,15 @@ protected function queryAuthorEntries(Builder $query, Collection $collection): v $blueprintsWithAuthor = $this->blueprintsWithAuthor($collection->entryBlueprints()); $blueprintsWithoutAuthor = $this->blueprintsWithoutAuthor($collection->entryBlueprints()); + if (empty($blueprintsWithAuthor)) { + return; + } + $query->where(fn ($query) => $query ->whereNotIn('collectionHandle', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections - ->when($blueprintsWithAuthor, fn ($query) => $query - ->orWhere(fn ($query) => $query - ->whereIn('blueprint', $blueprintsWithAuthor) - ->whereHas('author', fn ($subquery) => $subquery->where('id', User::current()->id())) - ) + ->orWhere(fn ($query) => $query + ->whereIn('blueprint', $blueprintsWithAuthor) + ->whereHas('author', fn ($subquery) => $subquery->where('id', User::current()->id())) ) ->orWhereIn('blueprint', $blueprintsWithoutAuthor) ); From aa96198a30526087df9849356edc4bd2c82ba1cd Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 12 Jan 2026 16:05:04 +0100 Subject: [PATCH 27/27] Make it more readable --- .../CP/Collections/QueriesAuthorEntries.php | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php index b05bbccf07a..749bc2ed0da 100644 --- a/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php +++ b/src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php @@ -2,7 +2,6 @@ namespace Statamic\Http\Controllers\CP\Collections; -use Illuminate\Support\Collection as SupportCollection; use Statamic\Contracts\Entries\Collection; use Statamic\Contracts\Query\Builder; use Statamic\Facades\User; @@ -12,33 +11,35 @@ trait QueriesAuthorEntries { protected function queryAuthorEntries(Builder $query, Collection $collection): void { - $blueprintsWithAuthor = $this->blueprintsWithAuthor($collection->entryBlueprints()); - $blueprintsWithoutAuthor = $this->blueprintsWithoutAuthor($collection->entryBlueprints()); + $blueprintsWithAuthor = $this->blueprintsWithAuthor($collection); if (empty($blueprintsWithAuthor)) { return; } $query->where(fn ($query) => $query - ->whereNotIn('collectionHandle', [$collection->handle()]) // Needed for entries fieldtypes configured for multiple collections + // Exclude entries from other collections (for entries fieldtypes with multiple collections) + ->whereNotIn('collectionHandle', [$collection->handle()]) + // Include entries with blueprints where the current user is the author ->orWhere(fn ($query) => $query ->whereIn('blueprint', $blueprintsWithAuthor) - ->whereHas('author', fn ($subquery) => $subquery->where('id', User::current()->id())) + ->whereHas('author', fn ($query) => $query->where('id', User::current()->id())) ) - ->orWhereIn('blueprint', $blueprintsWithoutAuthor) + // Include entries with blueprints that don't have an author + ->orWhereIn('blueprint', $this->blueprintsWithoutAuthor($collection)) ); } - protected function blueprintsWithAuthor(SupportCollection $blueprints): array + protected function blueprintsWithAuthor(Collection $collection): array { - return $blueprints + return $collection->entryBlueprints() ->filter(fn (Blueprint $blueprint) => $blueprint->hasField('author')) ->map->handle()->all(); } - protected function blueprintsWithoutAuthor(SupportCollection $blueprints): array + protected function blueprintsWithoutAuthor(Collection $collection): array { - return $blueprints + return $collection->entryBlueprints() ->filter(fn (Blueprint $blueprint) => ! $blueprint->hasField('author')) ->map->handle()->all(); }