diff --git a/src/Integrations/WordPress/Schema/FieldValidator.php b/src/Integrations/WordPress/Schema/FieldValidator.php index 2120379..8bcbbcb 100644 --- a/src/Integrations/WordPress/Schema/FieldValidator.php +++ b/src/Integrations/WordPress/Schema/FieldValidator.php @@ -137,28 +137,33 @@ final class FieldValidator : sprintf('Erlaubte Werte: %s.', implode(', ', $allowed)); } + /** + * The target must exist and be readable for the current user; an unreadable target gets + * the same answer as a missing one and its actual post type is never named (no probing). + */ private function checkRelation(FieldDefinition $field, mixed $value): ?string { if (! is_int($value) || $value <= 0) { return 'ID (positive Ganzzahl) erwartet.'; } + $actual = get_post_type($value); + $readable = $actual !== false && current_user_can('read_post', $value); $objectType = $field->related['object_type'] ?? ''; if ($objectType === 'media') { - return get_post_type($value) === 'attachment' + return $readable && $actual === 'attachment' ? null - : sprintf('%d ist kein Medium der Mediathek.', $value); + : sprintf('%d ist kein für dich lesbares Medium der Mediathek.', $value); } if ($objectType === 'post_type') { - $actual = get_post_type($value); - if ($actual === false) { - return sprintf('Eintrag %d existiert nicht.', $value); + if (! $readable) { + return sprintf('Eintrag %d existiert nicht oder ist für dich nicht lesbar.', $value); } $expected = $field->related['name'] ?? null; if ($expected !== null && $actual !== $expected) { - return sprintf('Eintrag %d ist vom Typ %s, erwartet %s.', $value, $actual, $expected); + return sprintf('Eintrag %d ist kein Eintrag vom Typ %s.', $value, $expected); } } diff --git a/tests/Unit/Integrations/WordPress/Schema/FieldValidatorTest.php b/tests/Unit/Integrations/WordPress/Schema/FieldValidatorTest.php index 661489e..4887f32 100644 --- a/tests/Unit/Integrations/WordPress/Schema/FieldValidatorTest.php +++ b/tests/Unit/Integrations/WordPress/Schema/FieldValidatorTest.php @@ -38,6 +38,7 @@ final class FieldValidatorTest extends TestCase public function testValidValuesPassUnchanged(): void { Functions\when('get_post_type')->alias(static fn (int $id) => [12 => 'lit_person', 3 => 'attachment'][$id] ?? false); + Functions\when('current_user_can')->justReturn(true); $values = ['role' => 'Leitung', 'level' => 'senior', 'mentor' => 12, 'photos' => [3], 'start' => '2026-10-01']; $result = (new FieldValidator())->validate($this->definitions(), $values, true); @@ -61,6 +62,7 @@ final class FieldValidatorTest extends TestCase public function testUnknownReadonlyAndInvalidValuesAreReported(): void { Functions\when('get_post_type')->alias(static fn (int $id) => [7 => 'page', 3 => 'attachment'][$id] ?? false); + Functions\when('current_user_can')->justReturn(true); $errors = (new FieldValidator())->validate($this->definitions(), [ 'role' => 'x', @@ -75,7 +77,7 @@ final class FieldValidatorTest extends TestCase self::assertStringContainsString('lit-content-types', $errors['nope']); self::assertSame('Feld ist schreibgeschützt.', $errors['secret']); self::assertSame('Erlaubte Werte: junior, senior.', $errors['level']); - self::assertSame('Eintrag 7 ist vom Typ page, erwartet lit_person.', $errors['mentor']); + self::assertSame('Eintrag 7 ist kein Eintrag vom Typ lit_person.', $errors['mentor']); self::assertSame('Liste erwartet.', $errors['photos']); self::assertSame('Datum im Format JJJJ-MM-TT erwartet.', $errors['start']); self::assertArrayNotHasKey('role', $errors); @@ -84,11 +86,24 @@ final class FieldValidatorTest extends TestCase public function testMissingRelatedPostAndNonMediaAreRejected(): void { Functions\when('get_post_type')->alias(static fn (int $id) => [5 => 'page'][$id] ?? false); + Functions\when('current_user_can')->justReturn(true); $errors = (new FieldValidator())->validate($this->definitions(), ['mentor' => 99, 'photos' => [5]], false)['errors']; - self::assertSame('Eintrag 99 existiert nicht.', $errors['mentor']); - self::assertSame('5 ist kein Medium der Mediathek.', $errors['photos']); + self::assertSame('Eintrag 99 existiert nicht oder ist für dich nicht lesbar.', $errors['mentor']); + self::assertSame('5 ist kein für dich lesbares Medium der Mediathek.', $errors['photos']); + } + + public function testUnreadableTargetsLookLikeMissingOnesAndRevealNoType(): void + { + Functions\when('get_post_type')->alias(static fn (int $id) => [8 => 'lit_secret', 4 => 'attachment'][$id] ?? false); + Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => $cap === 'read_post' && ! in_array($id, [8, 4], true)); + + $errors = (new FieldValidator())->validate($this->definitions(), ['mentor' => 8, 'photos' => [4]], false)['errors']; + + self::assertSame('Eintrag 8 existiert nicht oder ist für dich nicht lesbar.', $errors['mentor']); + self::assertStringNotContainsString('lit_secret', $errors['mentor']); + self::assertSame('4 ist kein für dich lesbares Medium der Mediathek.', $errors['photos']); } public function testPasswordFieldsAreValidatedAsStrings(): void