fix: full-form internal links follow the {{link:}} target rules (ruling P10)

One PostIdParser for {{link:}} and select_post; post_link_url_<type> needs a
living, readable, non-attachment target of exactly that type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 14:54:51 +02:00
co-authored by Claude Opus 5.5
parent c615522cf6
commit 06f13799de
14 changed files with 229 additions and 28 deletions
@@ -449,6 +449,64 @@ final class VariableRefTest extends TestCase
}
}
/** Ruling P10: a full-form internal link follows the {{link:}} target rules, its type included. */
public function testFullFormInternalLinksFollowTheLinkTargetRules(): void
{
$ref = self::ref([
'post_link_url_page' => ['post_id' => 'select_post'],
'post_link_url_post' => ['post_id' => 'select_post'],
]);
$sanitizer = ComposeFixture::sanitizer();
self::assertSame(
self::content('post_link_url_post', '{"post_id":"840"}'),
$ref->canonical(self::content('post_link_url_post', '{"post_id":"840"}'), $sanitizer, true)
);
self::assertSame(
self::content('post_link_url_post', '{"post_id":840}'),
$ref->canonical(self::content('post_link_url_post', '{"post_id":840}'), $sanitizer, true)
);
$refused = [
'a post is no page' => self::content('post_link_url_page', '{"post_id":"840"}'),
'a post is no page (int)' => self::content('post_link_url_page', '{"post_id":840}'),
'a page is no post' => self::content('post_link_url_post', '{"post_id":"12"}'),
'no link target' => self::content('post_link_url_page', '{"post_id":"666"}'),
'attachment' => self::content('post_link_url_page', '{"post_id":"667"}'),
'overflowing id' => self::content('post_link_url_page', '{"post_id":"99999999999999999999"}'),
'padded id' => self::content('post_link_url_page', '{"post_id":"0840"}'),
'float id' => self::content('post_link_url_page', '{"post_id":12.0}'),
'bool id' => self::content('post_link_url_page', '{"post_id":true}'),
];
foreach ($refused as $case => $value) {
try {
$ref->canonical($value, $sanitizer, true);
self::fail($case . ' was accepted');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString('Beitrags-ID', $e->getMessage(), $case);
}
}
}
/** Ruling S37/P10: select_post outside an internal link stays "a post the user may read", of any type. */
public function testOtherPostSettingsOnlyNeedAReadablePost(): void
{
$ref = self::ref(['evil' => ['p' => 'select_post']]);
$sanitizer = ComposeFixture::sanitizer();
self::assertSame(
self::content('evil', '{"p":"840"}'),
$ref->canonical(self::content('evil', '{"p":"840"}'), $sanitizer)
);
foreach (['"666"', '"0840"', '"99999999999999999999"'] as $id) {
try {
$ref->canonical(self::content('evil', '{"p":' . $id . '}'), $sanitizer);
self::fail($id . ' was accepted');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString('Beitrags-ID', $e->getMessage(), $id);
}
}
}
/** E13a: an internal link (post_link_url_<type>) is a URL – refused in every other field. */
public function testInternalLinksOnlyInUrlFields(): void
{