Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/FormatGroupMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ public function __invoke(Renderer $renderer, mixed $context, string $xml): strin
return Utils::replaceAttributes($xml, 'GROUPMENTION', function ($attributes) use ($context) {
/** @var Group|null $group */
$group = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsGroups') => $context->relationLoaded('mentionsGroups')
? $context->mentionsGroups->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsGroups()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsGroups') => $context->mentionsGroups->find($attributes['id']), // @phpstan-ignore-line
default => Group::query()->find($attributes['id']),
};

Expand Down
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/FormatPostMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ public function __invoke(Renderer $renderer, mixed $context, string $xml): strin
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
/** @var Post|null $post */
$post = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsPosts') => $context->relationLoaded('mentionsPosts')
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsPosts()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsPosts') => $context->mentionsPosts->find($attributes['id']), // @phpstan-ignore-line
default => Post::query()->find($attributes['id']),
};

Expand Down
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/FormatTagMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ public function __invoke(Renderer $renderer, mixed $context, string $xml, ?Reque
return Utils::replaceAttributes($xml, 'TAGMENTION', function ($attributes) use ($context) {
/** @var Tag|null $tag */
$tag = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsTags') => $context->relationLoaded('mentionsTags')
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsTags()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsTags') => $context->mentionsTags->find($attributes['id']), // @phpstan-ignore-line
default => Tag::query()->find($attributes['id']),
};

Expand Down
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/FormatUserMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ public function __invoke(Renderer $renderer, mixed $context, string $xml): strin
return Utils::replaceAttributes($xml, 'USERMENTION', function ($attributes) use ($context) {
/** @var User|null $user */
$user = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsUsers') => $context->relationLoaded('mentionsUsers')
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsUsers()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsUsers') => $context->mentionsUsers->find($attributes['id']), // @phpstan-ignore-line
default => User::query()->find($attributes['id']),
};

Expand Down
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/UnparsePostMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ protected function updatePostMentionTags(mixed $context, string $xml): string
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
/** @var Post|null $post */
$post = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsPosts') => $context->relationLoaded('mentionsPosts')
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsPosts()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsPosts') => $context->mentionsPosts->find($attributes['id']), // @phpstan-ignore-line
default => Post::query()->find($attributes['id']),
};

Expand Down
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/UnparseTagMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ protected function updateTagMentionTags(mixed $context, string $xml): string
return Utils::replaceAttributes($xml, 'TAGMENTION', function (array $attributes) use ($context) {
/** @var Tag|null $tag */
$tag = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsTags') => $context->relationLoaded('mentionsTags')
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsTags()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsTags') => $context->mentionsTags->find($attributes['id']), // @phpstan-ignore-line
default => Tag::query()->find($attributes['id']),
};

Expand Down
4 changes: 1 addition & 3 deletions extensions/mentions/src/Formatter/UnparseUserMentions.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ protected function updateUserMentionTags(mixed $context, string $xml): string
return Utils::replaceAttributes($xml, 'USERMENTION', function ($attributes) use ($context) {
/** @var User|null $user */
$user = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsUsers') => $context->relationLoaded('mentionsUsers')
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsUsers()->find($attributes['id']), // @phpstan-ignore-line
$context instanceof AbstractModel && $context->relationLoaded('mentionsUsers') => $context->mentionsUsers->find($attributes['id']), // @phpstan-ignore-line
default => User::query()->find($attributes['id']),
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Mentions\Tests\integration\api;

use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Post\Event\Saving;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use PHPUnit\Framework\Attributes\Test;

/**
* Regression tests for https://github.com/flarum/framework/issues/4823.
*
* When `$post->content` is read during the `Saving` lifecycle event (before
* the post's mention pivot tables have been populated), the mention
* formatters must still resolve the mentioned entity by its own existence
* rather than the not-yet-synced pivot — otherwise the mention is wrongly
* unparsed to the `[deleted]` / `[unknown]` fallback text.
*/
class MentionsDuringSavingTest extends TestCase
{
use RetrievesAuthorizedUsers;

/**
* Content observed by the Saving listener, captured for assertion.
*/
protected ?string $observedContent = null;

protected function setUp(): void
{
parent::setUp();

$this->extension('flarum-mentions');

$this->extend(
(new Extend\Event())
->listen(Saving::class, function (Saving $event) {
// Reading content here triggers unparse() while the post's
// mention pivots are not yet synced — the #4823 scenario.
$this->observedContent = $event->post->content;
})
);

$this->prepareDatabase([
User::class => [
$this->normalUser(),
['id' => 3, 'username' => 'mentioned_user', 'email' => 'mentioned_user@machine.local', 'is_email_confirmed' => 1],
],
Discussion::class => [
['id' => 1, 'title' => 'Test discussion', 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
],
Post::class => [
['id' => 1, 'number' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Original</p></t>'],
],
]);
}

#[Test]
public function user_mention_resolves_during_saving_of_a_new_post(): void
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '@"mentioned_user"#3',
],
'relationships' => [
'discussion' => ['data' => ['type' => 'discussions', 'id' => '1']],
],
],
],
])
);

$this->assertEquals(201, $response->getStatusCode(), $response->getBody()->getContents());

// The Saving listener must have seen the real mention, NOT the deleted
// fallback — this is the regression from #4823.
$this->assertNotNull($this->observedContent);
$this->assertStringContainsString('mentioned_user', $this->observedContent);
$this->assertStringNotContainsString('[deleted]', $this->observedContent);
$this->assertStringNotContainsString('[unknown]', $this->observedContent);
}

#[Test]
public function user_mention_resolves_during_saving_when_editing_a_post(): void
{
$response = $this->send(
$this->request('PATCH', '/api/posts/1', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '@"mentioned_user"#3 edited',
],
],
],
])
);

$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());

$this->assertNotNull($this->observedContent);
$this->assertStringContainsString('mentioned_user', $this->observedContent);
$this->assertStringNotContainsString('[deleted]', $this->observedContent);
$this->assertStringNotContainsString('[unknown]', $this->observedContent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ public function editing_a_post_with_deleted_author_that_has_a_mention_works()
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#p9',
'content' => '@"Bad _ User"#p9 edited',
],
],
],
Expand All @@ -494,7 +494,7 @@ public function editing_a_post_with_deleted_author_that_has_a_mention_works()
$response = json_decode($body, true);

$this->assertStringContainsString('Bad "#p6 User', $response['data']['attributes']['contentHtml']);
$this->assertEquals('@"Bad _ User"#p9', $response['data']['attributes']['content']);
$this->assertEquals('@"Bad _ User"#p9 edited', $response['data']['attributes']['content']);
$this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(9));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected function setUp(): void
],
Post::class => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><TAGMENTION id="1" slug="test_old_slug" tagname="TestOldName">#test_old_slug</TAGMENTION></r>'],
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="3" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="999" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="2020" slug="i_am_a_deleted_tag" tagname="i_am_a_deleted_tag">#i_am_a_deleted_tag</TAGMENTION></r>'],
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="5" slug="laravel">#laravel</TAGMENTION></r>'],
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ public function editing_a_post_with_deleted_author_that_has_a_mention_works()
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#5',
'content' => '@"Bad _ User"#5 edited',
],
],
],
Expand All @@ -479,7 +479,7 @@ public function editing_a_post_with_deleted_author_that_has_a_mention_works()
$response = json_decode($body, true);

$this->assertStringContainsString('Bad "#p6 User', $response['data']['attributes']['contentHtml']);
$this->assertEquals('@"Bad _ User"#5', $response['data']['attributes']['content']);
$this->assertEquals('@"Bad _ User"#5 edited', $response['data']['attributes']['content']);
$this->assertStringContainsString('UserMention', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsUsers->find(5));
}
Expand Down
Loading