diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 8c74b5004..2e7b64784 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -128,9 +128,6 @@ return [ ->render(Formatter\FormatTagMentions::class) ->unparse(Formatter\UnparseTagMentions::class), - (new Extend\Model(Post::class)) - ->belongsToMany('mentionsTags', Tag::class, 'post_mentions_tag', 'post_id', 'mentions_tag_id'), - (new Extend\ApiSerializer(BasicPostSerializer::class)) ->hasMany('mentionsTags', TagSerializer::class), diff --git a/extensions/tags/extend.php b/extensions/tags/extend.php index 8f2d875c3..4ca124e5b 100644 --- a/extensions/tags/extend.php +++ b/extensions/tags/extend.php @@ -8,6 +8,7 @@ */ use Flarum\Api\Controller as FlarumController; +use Flarum\Api\Serializer\BasicPostSerializer; use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\ForumSerializer; use Flarum\Discussion\Discussion; @@ -18,6 +19,7 @@ use Flarum\Extend; use Flarum\Flags\Api\Controller\ListFlagsController; use Flarum\Http\RequestUtil; use Flarum\Post\Filter\PostFilterer; +use Flarum\Post\Post; use Flarum\Tags\Access; use Flarum\Tags\Api\Controller; use Flarum\Tags\Api\Serializer\TagSerializer; @@ -142,4 +144,37 @@ return [ (new Extend\ModelUrl(Tag::class)) ->addSlugDriver('default', Utf8SlugDriver::class), + + /* + * Fixes DiscussionTaggedPost showing tags as deleted because they are not loaded in the store. + * @link https://github.com/flarum/framework/issues/3620#issuecomment-1232911734 + */ + + (new Extend\Model(Post::class)) + ->belongsToMany('mentionsTags', Tag::class, 'post_mentions_tag', 'post_id', 'mentions_tag_id') + // We do not wish to include all `mentionsTags` in the API response, + // only those related to `discussionTagged` posts. + ->relationship('eventPostMentionsTags', function (Post $model) { + return $model->mentionsTags(); + }), + + (new Extend\ApiSerializer(BasicPostSerializer::class)) + ->relationship('eventPostMentionsTags', function (BasicPostSerializer $serializer, Post $model) { + if ($model instanceof DiscussionTaggedPost) { + return $serializer->hasMany($model, TagSerializer::class, 'eventPostMentionsTags'); + } + + return null; + }) + ->hasMany('eventPostMentionsTags', TagSerializer::class), + + (new Extend\ApiController(FlarumController\ListPostsController::class)) + ->addInclude('eventPostMentionsTags') + // Restricted tags should still appear as `deleted` to unauthorized users. + ->loadWhere('eventPostMentionsTags', function ($query, ?ServerRequestInterface $request) { + if ($request) { + $actor = RequestUtil::getActor($request); + $query->whereVisibleTo($actor); + } + }), ]; diff --git a/extensions/tags/src/Post/DiscussionTaggedPost.php b/extensions/tags/src/Post/DiscussionTaggedPost.php index 0dd458703..db482e561 100755 --- a/extensions/tags/src/Post/DiscussionTaggedPost.php +++ b/extensions/tags/src/Post/DiscussionTaggedPost.php @@ -45,6 +45,11 @@ class DiscussionTaggedPost extends AbstractEventPost implements MergeableInterfa $this->save(); + // Create mentions of the tags so that we can load them when rendering. + $this->mentionsTags()->sync( + array_merge($this->content[0], $this->content[1]) + ); + return $this; } diff --git a/extensions/tags/tests/integration/api/posts/ListTest.php b/extensions/tags/tests/integration/api/posts/ListTest.php new file mode 100644 index 000000000..efd331bf2 --- /dev/null +++ b/extensions/tags/tests/integration/api/posts/ListTest.php @@ -0,0 +1,137 @@ +<?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\Tags\Tests\integration\api\posts; + +use Flarum\Tags\Tests\integration\RetrievesRepresentativeTags; +use Flarum\Testing\integration\RetrievesAuthorizedUsers; +use Flarum\Testing\integration\TestCase; + +class ListTest extends TestCase +{ + use RetrievesAuthorizedUsers; + use RetrievesRepresentativeTags; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->extension('flarum-tags'); + + $this->prepareDatabase([ + 'tags' => $this->tags(), + 'users' => [ + $this->normalUser(), + [ + 'id' => 3, + 'username' => 'normal3', + 'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure" + 'email' => 'normal3@machine.local', + 'is_email_confirmed' => 1, + ] + ], + 'groups' => [ + ['id' => 100, 'name_singular' => 'acme', 'name_plural' => 'acme'] + ], + 'group_user' => [ + ['group_id' => 100, 'user_id' => 2] + ], + 'group_permission' => [ + ['group_id' => 100, 'permission' => 'tag5.viewForum'], + ], + 'discussions' => [ + ['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1], + ], + 'posts' => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'number' => 1], + ['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'discussionTagged', 'content' => '[[1,5],[5]]', 'number' => 2], + ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'number' => 3], + ['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'discussionTagged', 'content' => '[[1,5],[5]]', 'number' => 4], + ], + 'discussion_tag' => [ + ['discussion_id' => 1, 'tag_id' => 1], + ], + 'post_mentions_tag' => [ + ['post_id' => 2, 'mentions_tag_id' => 1], + ['post_id' => 2, 'mentions_tag_id' => 5], + ['post_id' => 4, 'mentions_tag_id' => 1], + ['post_id' => 4, 'mentions_tag_id' => 5], + ], + ]); + } + + /** + * @dataProvider authorizedUsers + * @test + */ + public function event_mentioned_tags_are_included_in_response_for_authorized_users(int $userId) + { + $response = $this->send( + $this->request('GET', '/api/posts', [ + 'authenticatedAs' => $userId + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true); + + $tagIds = array_map(function ($tag) { + return $tag['id']; + }, array_filter($data['included'], function ($item) { + return $item['type'] === 'tags'; + })); + + $this->assertEqualsCanonicalizing([1, 5], $tagIds); + } + + /** + * @dataProvider unauthorizedUsers + * @test + */ + public function event_mentioned_tags_are_not_included_in_response_for_unauthorized_users(?int $userId) + { + $response = $this->send( + $this->request('GET', '/api/posts', [ + 'authenticatedAs' => $userId + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $data = json_decode($response->getBody()->getContents(), true); + + $tagIds = array_map(function ($tag) { + return $tag['id']; + }, array_filter($data['included'], function ($item) { + return $item['type'] === 'tags'; + })); + + $this->assertEqualsCanonicalizing([1], $tagIds); + } + + public function authorizedUsers() + { + return [ + 'admin' => [1], + 'normal user with permission' => [2], + ]; + } + + public function unauthorizedUsers() + { + return [ + 'normal user without permission' => [3], + 'guest' => [null], + ]; + } +}