Skip to content

Commit bd528dc

Browse files
jasonvargaclaude
andcommitted
[5.x] Don't expose unviewable taxonomy columns via relationship listing
Mirrors the collection fix: term relationship columns are now derived only from taxonomies the user can view, falling back to the default columns otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a68c119 commit bd528dc

2 files changed

Lines changed: 88 additions & 8 deletions

File tree

src/Fieldtypes/Terms.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,21 +291,32 @@ private function getViewableTaxonomies(array $taxonomies): Collection
291291

292292
public function getResourceCollection($request, $items)
293293
{
294-
// With no viewable taxonomies we return empty data and, crucially, no columns. The
295-
// columns would otherwise be derived from the first configured taxonomy's blueprint,
296-
// which the user isn't allowed to view.
297-
if ($this->getViewableTaxonomies($this->getConfiguredTaxonomies())->isEmpty()) {
294+
// Derive columns only from a taxonomy the user can view. With none viewable, return
295+
// empty data and no columns rather than leaking the structure of an unviewable blueprint.
296+
if (! $taxonomy = $this->getColumnTaxonomy($request)) {
298297
return JsonResource::collection($items)->additional(['meta' => ['columns' => []]]);
299298
}
300299

301300
return (new TermsResource($items, $this))
302-
->blueprint($this->getBlueprint($request))
303-
->columnPreferenceKey("taxonomies.{$this->getFirstTaxonomyFromRequest($request)->handle()}.columns");
301+
->blueprint($taxonomy->termBlueprint())
302+
->columnPreferenceKey("taxonomies.{$taxonomy->handle()}.columns");
303+
}
304+
305+
protected function getBlueprint($request = null)
306+
{
307+
return $this->getColumnTaxonomy($request)?->termBlueprint();
304308
}
305309

306-
protected function getBlueprint($request)
310+
protected function getColumnTaxonomy($request = null)
307311
{
308-
return $this->getFirstTaxonomyFromRequest($request)->termBlueprint();
312+
$taxonomy = $this->getFirstTaxonomyFromRequest($request);
313+
314+
// Only derive columns from a taxonomy the user can view. If the first configured
315+
// taxonomy isn't viewable, fall back to the first viewable configured taxonomy,
316+
// or none at all when the user can view none of them.
317+
return User::current()->can('view', $taxonomy)
318+
? $taxonomy
319+
: $this->getViewableTaxonomies($this->getConfiguredTaxonomies())->first();
309320
}
310321

311322
protected function getFirstTaxonomyFromRequest($request)

tests/Feature/Fieldtypes/RelationshipFieldtypeTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tests\Feature\Fieldtypes;
44

55
use PHPUnit\Framework\Attributes\Test;
6+
use Statamic\Facades\Blueprint;
67
use Statamic\Facades\Collection;
78
use Statamic\Facades\Entry;
89
use Statamic\Facades\Form;
@@ -199,6 +200,74 @@ public function it_returns_an_empty_listing_when_the_user_cannot_view_any_of_the
199200
$this->assertEmpty($response->json('meta.columns'));
200201
}
201202

203+
#[Test]
204+
public function it_does_not_expose_columns_from_an_unviewable_taxonomy_blueprint()
205+
{
206+
Taxonomy::make('secret')->title('Secret')->save();
207+
Taxonomy::make('topics')->title('Topics')->save();
208+
Blueprint::make('secret')
209+
->setNamespace('taxonomies.secret')
210+
->setContents(['fields' => [
211+
['handle' => 'classified', 'field' => ['type' => 'text']],
212+
]])
213+
->save();
214+
Blueprint::make('topics')
215+
->setNamespace('taxonomies.topics')
216+
->setContents(['fields' => [
217+
['handle' => 'summary', 'field' => ['type' => 'text']],
218+
]])
219+
->save();
220+
221+
// The user can view the second configured taxonomy but not the first.
222+
$this->setTestRoles(['test' => ['access cp', 'view topics terms']]);
223+
$user = User::make()->assignRole('test')->save();
224+
225+
$config = base64_encode(json_encode([
226+
'type' => 'terms',
227+
'taxonomies' => ['secret', 'topics'],
228+
]));
229+
230+
$response = $this
231+
->actingAs($user)
232+
->getJson("/cp/fieldtypes/relationship?config={$config}")
233+
->assertOk();
234+
235+
$columns = collect($response->json('meta.columns'))->pluck('field')->all();
236+
237+
// Columns come from the viewable taxonomy, never the unviewable first one.
238+
$this->assertNotContains('classified', $columns);
239+
$this->assertContains('summary', $columns);
240+
}
241+
242+
#[Test]
243+
public function an_authorized_user_still_gets_the_full_taxonomy_columns()
244+
{
245+
Taxonomy::make('secret')->title('Secret')->save();
246+
Blueprint::make('secret')
247+
->setNamespace('taxonomies.secret')
248+
->setContents(['fields' => [
249+
['handle' => 'classified', 'field' => ['type' => 'text']],
250+
]])
251+
->save();
252+
253+
$this->setTestRoles(['test' => ['access cp', 'view secret terms']]);
254+
$user = User::make()->assignRole('test')->save();
255+
256+
$config = base64_encode(json_encode([
257+
'type' => 'terms',
258+
'taxonomies' => ['secret'],
259+
]));
260+
261+
$response = $this
262+
->actingAs($user)
263+
->getJson("/cp/fieldtypes/relationship?config={$config}")
264+
->assertOk();
265+
266+
$columns = collect($response->json('meta.columns'))->pluck('field')->all();
267+
268+
$this->assertContains('classified', $columns);
269+
}
270+
202271
#[Test]
203272
public function it_scopes_collection_listing_to_viewable_collections()
204273
{

0 commit comments

Comments
 (0)