Skip to content
Open
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
71120eb
Add permission
aerni Jun 12, 2025
39950db
Restrict other authors from viewing entries
aerni Jun 12, 2025
91b22dd
Only show author’s entries in listings
aerni Jun 12, 2025
0544e44
Fix entries count
aerni Jun 12, 2025
9aa2719
Make Erin happy with shorter syntax
aerni Jun 13, 2025
ba02dce
Handle entries fieldtype
aerni Jun 13, 2025
9fd7f56
Handle tree view
aerni Jun 13, 2025
3fade8b
🍺
aerni Jun 13, 2025
6f7b790
Move filtering to the controller
aerni Jun 13, 2025
24d5892
Fix issue with multiple collections
aerni Jun 13, 2025
3006d6f
Dynamically change author field visibility
aerni Jun 13, 2025
e114128
Also remove author column
aerni Jun 13, 2025
8b3c4f2
Fix tests
aerni Jun 25, 2025
38090d5
Add update script
aerni Jun 25, 2025
f718e21
Add support for multiple authors
aerni Jun 27, 2025
fe47c01
Abstract
aerni Jun 27, 2025
2cddf2a
Fix tests
aerni Jun 27, 2025
475dbdd
Add tests
aerni Jun 27, 2025
6e53818
Allow users to override field visibility
aerni Aug 5, 2025
a5f0cea
Register event listener
aerni Aug 5, 2025
7d9bc30
Make sure permissions apply to search
aerni Aug 20, 2025
773efd6
Merge remote-tracking branch 'statamic/5.x' into feature/view-other-a…
aerni Jan 12, 2026
50f3233
Compare against string
aerni Jan 12, 2026
75d76d0
Use new whereHas
aerni Jan 12, 2026
6e732e2
Fix issue if there are no blueprints with author
aerni Jan 12, 2026
1439cdc
Fix test
aerni Jan 12, 2026
3674ed5
No need to add queries if there is no author field
aerni Jan 12, 2026
aa96198
Make it more readable
aerni Jan 12, 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
1 change: 1 addition & 0 deletions resources/lang/en/permissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions src/Auth/CorePermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
28 changes: 28 additions & 0 deletions src/Entries/ChangeAuthorFieldVisibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Statamic\Entries;

use Statamic\Contracts\Entries\Entry;
use Statamic\Events\EntryBlueprintFound;

class ChangeAuthorFieldVisibility
{
public function handle(EntryBlueprintFound $event)
{
if (! $event->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]);
}
}
9 changes: 8 additions & 1 deletion src/Fieldtypes/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,7 +32,8 @@

class Entries extends Relationship
{
use QueriesFilters;
use QueriesAuthorEntries,
QueriesFilters;

protected $categories = ['relationship'];
protected $keywords = ['entry'];
Expand Down Expand Up @@ -144,6 +146,11 @@ 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(fn ($collection) => $this->queryAuthorEntries($query, $collection));

$this->activeFilterBadges = $this->queryFilters($query, $filters, $this->getSelectionFilterContext());

if ($sort = $this->getSortColumn($request)) {
Expand Down
13 changes: 13 additions & 0 deletions src/Http/Controllers/CP/Collections/CollectionTreeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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];
}

Expand Down
12 changes: 11 additions & 1 deletion src/Http/Controllers/CP/Collections/CollectionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.'));
Expand Down Expand Up @@ -55,10 +57,18 @@ 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]),
fn ($query) => $this->queryAuthorEntries($query, $collection)
)
->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()),
Expand Down
29 changes: 13 additions & 16 deletions src/Http/Controllers/CP/Collections/EntriesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
class EntriesController extends CpController
{
use ExtractsFromEntryFields,
QueriesAuthorEntries,
QueriesFilters;

public function index(FilteredRequest $request, $collection)
Expand Down Expand Up @@ -70,21 +71,25 @@ 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());
}

if (User::current()->cant('view-other-authors-entries', [EntryContract::class, $collection])) {
$this->queryAuthorEntries($query, $collection);
}

return $query;
}

Expand All @@ -103,10 +108,6 @@ 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']);
}

[$values, $meta, $extraValues] = $this->extractFromFields($entry, $blueprint);

if ($hasOrigin = $entry->hasOrigin()) {
Expand Down Expand Up @@ -302,10 +303,6 @@ 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']);
}

$values = Entry::make()->collection($collection)->values()->all();

if ($collection->hasStructure() && $request->parent) {
Expand Down
46 changes: 46 additions & 0 deletions src/Http/Controllers/CP/Collections/QueriesAuthorEntries.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Statamic\Http\Controllers\CP\Collections;

use Statamic\Contracts\Entries\Collection;
use Statamic\Contracts\Query\Builder;
use Statamic\Facades\User;
use Statamic\Fields\Blueprint;

trait QueriesAuthorEntries
{
protected function queryAuthorEntries(Builder $query, Collection $collection): void
{
$blueprintsWithAuthor = $this->blueprintsWithAuthor($collection);

if (empty($blueprintsWithAuthor)) {
return;
}

$query->where(fn ($query) => $query
// 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 ($query) => $query->where('id', User::current()->id()))
)
// Include entries with blueprints that don't have an author
->orWhereIn('blueprint', $this->blueprintsWithoutAuthor($collection))
);
}

protected function blueprintsWithAuthor(Collection $collection): array
{
return $collection->entryBlueprints()
->filter(fn (Blueprint $blueprint) => $blueprint->hasField('author'))
->map->handle()->all();
}

protected function blueprintsWithoutAuthor(Collection $collection): array
{
return $collection->entryBlueprints()
->filter(fn (Blueprint $blueprint) => ! $blueprint->hasField('author'))
->map->handle()->all();
}
}
6 changes: 6 additions & 0 deletions src/Http/Resources/CP/Entries/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions src/Policies/EntryPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -49,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();
Expand Down
1 change: 1 addition & 0 deletions src/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions src/UpdateScripts/AddViewOtherAuthorsEntriesPermissions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Statamic\UpdateScripts;

use Statamic\Facades\Role;

class AddViewOtherAuthorsEntriesPermissions extends UpdateScript
{
public function shouldUpdate($newVersion, $oldVersion)
{
return $this->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();
}
}
Loading
Loading